From 6c1a25d4911021e66a121ff6b7e2b0b54795d8e8 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Mon, 21 Sep 2015 02:43:57 +0200 Subject: [PATCH 1/5] Support base paths Fixes #315 --- client/router.js | 45 +++++++++++++++++----------- test/client/router.core.spec.js | 53 +++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/client/router.js b/client/router.js index 04497d3..e461ce5 100644 --- a/client/router.js +++ b/client/router.js @@ -14,7 +14,7 @@ Router = function () { // holds onRoute callbacks this._onRouteCallbacks = []; - // if _askedToWait is true. We don't automatically start the router + // if _askedToWait is true. We don't automatically start the router // in Meteor.startup callback. (see client/_init.js) // Instead user need to call `.initialize() this._askedToWait = false; @@ -27,11 +27,16 @@ Router = function () { this.notFound = this.notfound = null; // indicate it's okay (or not okay) to run the tracker // when doing subscriptions - // using a number and increment it help us to support FlowRouter.go() + // using a number and increment it help us to support FlowRouter.go() // and legitimate reruns inside tracker on the same event loop. // this is a solution for #145 this.safeToRun = 0; + // Meteor exposes to the client the path prefix that was defined using the + // ROOT_URL environement variable on the server using the global runtime + // configuration. See #315. + this._basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; + // this is a chain contains a list of old routes // most of the time, there is only one old route // but when it's the time for a trigger redirect we've a chain @@ -69,9 +74,9 @@ Router.prototype.route = function(pathDef, options, group) { self._oldRouteChain.push(oldRoute); var queryParams = self._qs.parse(context.querystring); - // _qs.parse() gives us a object without prototypes, + // _qs.parse() gives us a object without prototypes, // created with Object.create(null) - // Meteor's check doesn't play nice with it. + // Meteor's check doesn't play nice with it. // So, we need to fix it by cloning it. // see more: https://github.com/meteorhacks/flow-router/issues/164 queryParams = JSON.parse(JSON.stringify(queryParams)); @@ -94,9 +99,9 @@ Router.prototype.route = function(pathDef, options, group) { var triggers = self._triggersEnter.concat(route._triggersEnter); Triggers.runTriggers( - triggers, - self._current, - self._redirectFn, + triggers, + self._current, + self._redirectFn, afterAllTriggersRan ); }; @@ -143,7 +148,7 @@ Router.prototype.path = function(pathDef, fields, queryParams) { // this is to allow page js to keep the custom characters as it is // we need to encode 2 times otherwise "/" char does not work properly - // So, in that case, when I includes "/" it will think it's a part of the + // So, in that case, when I includes "/" it will think it's a part of the // route. encoding 2times fixes it return encodeURIComponent(encodeURIComponent(fields[key] || "")); }); @@ -169,7 +174,7 @@ Router.prototype.path = function(pathDef, fields, queryParams) { Router.prototype.go = function(pathDef, fields, queryParams) { var path = this.path(pathDef, fields, queryParams); - + var useReplaceState = this.env.replaceState.get(); if(useReplaceState) { this._page.replace(path); @@ -234,7 +239,7 @@ Router.prototype.current = function() { // Implementing Reactive APIs var reactiveApis = [ - 'getParam', 'getQueryParam', + 'getParam', 'getQueryParam', 'getRouteName', 'watchPathChange' ]; reactiveApis.forEach(function(api) { @@ -343,11 +348,11 @@ Router.prototype.initialize = function(options) { // by overriding page.js`s "show" method. // Why? // It is impossible to bypass exit triggers, - // becuase they execute before the handler and + // because they execute before the handler and // can not know what the next path is, inside exit trigger. // // we need override both show, replace to make this work - // since we use redirect when we are talking about withReplaceState + // since we use redirect when we are talking about withReplaceState _.each(['show', 'replace'], function(fnName) { var original = self._page[fnName]; self._page[fnName] = function(path, state, dispatch, push) { @@ -364,7 +369,13 @@ Router.prototype.initialize = function(options) { // in unpredicatable manner. See #168 // this is the default behaviour and we need keep it like that // we are doing a hack. see .path() - this._page({decodeURLComponents: true, hashbang: !!options.hashbang}); + this._page({ + basePath: this._basePath, + decodeURLComponents: true, + hashbang: !!options.hashbang + }); + this._page.base(this._basePath); + this._initialized = true; }; @@ -423,7 +434,7 @@ Router.prototype._buildTracker = function() { if(isRouteChange) { // We need to trigger that route (definition itself) has changed. // So, we need to re-run all the register callbacks to current route - // This is pretty important, otherwise tracker + // This is pretty important, otherwise tracker // can't identify new route's items // We also need to afterFlush, otherwise this will re-run @@ -468,9 +479,9 @@ Router.prototype._invalidateTracker = function() { // XXX: fix this with a proper solution by removing subscription mgt. // from the router. Then we don't need to run invalidate using a tracker - // this happens when we are trying to invoke a route change + // this happens when we are trying to invoke a route change // with inside a route chnage. (eg:- Template.onCreated) - // Since we use page.js and tracker, we don't have much control + // Since we use page.js and tracker, we don't have much control // over this process. // only solution is to defer route execution. @@ -542,7 +553,7 @@ Router.prototype.onRouteRegister = function(cb) { Router.prototype._triggerRouteRegister = function(currentRoute) { // We should only need to send a safe set of fields on the route // object. - // This is not to hide what's inside the route object, but to show + // This is not to hide what's inside the route object, but to show // these are the public APIs var routePublicApi = _.pick(currentRoute, 'name', 'pathDef', 'path'); var omittingOptionFields = [ diff --git a/test/client/router.core.spec.js b/test/client/router.core.spec.js index 0668d4e..22ef358 100644 --- a/test/client/router.core.spec.js +++ b/test/client/router.core.spec.js @@ -346,7 +346,7 @@ Tinytest.addAsync('Client - Router - notFound', function (test, done) { }, 50); }); -Tinytest.addAsync('Client - Router - withReplaceState - enabled', +Tinytest.addAsync('Client - Router - withReplaceState - enabled', function (test, done) { var pathDef = "/" + Random.id() + "/:id"; var originalRedirect = FlowRouter._page.replace; @@ -362,7 +362,7 @@ function (test, done) { test.equal(params.id, "awesome"); test.equal(callCount, 1); FlowRouter._page.replace = originalRedirect; - // We don't use Meteor.defer here since it carries + // We don't use Meteor.defer here since it carries // Meteor.Environment vars too // Which breaks our test below setTimeout(done, 0); @@ -374,7 +374,7 @@ function (test, done) { }); }); -Tinytest.addAsync('Client - Router - withReplaceState - disabled', +Tinytest.addAsync('Client - Router - withReplaceState - disabled', function (test, done) { var pathDef = "/" + Random.id() + "/:id"; var originalRedirect = FlowRouter._page.replace; @@ -537,7 +537,7 @@ function (test, next) { }); Tinytest.addAsync( -'Client - Router - wait - before initialize', +'Client - Router - wait - before initialize', function(test, done) { FlowRouter._initialized = false; FlowRouter.wait(); @@ -549,7 +549,7 @@ function(test, done) { }); Tinytest.addAsync( -'Client - Router - wait - after initialized', +'Client - Router - wait - after initialized', function(test, done) { try { FlowRouter.wait(); @@ -560,7 +560,7 @@ function(test, done) { }); Tinytest.addAsync( -'Client - Router - initialize - after initialized', +'Client - Router - initialize - after initialized', function(test, done) { try { FlowRouter.initialize(); @@ -570,6 +570,47 @@ function(test, done) { } }); +Tinytest.addAsync( +'Client - Router - base path - url updated', +function(test, done) { + var simulatedBasePath = '/flow'; + var previousBasePath = FlowRouter._basePath; + var rand = Random.id(); + FlowRouter.route('/' + rand, { action: function() {} }); + + setBasePath(simulatedBasePath); + FlowRouter.go('/' + rand); + setTimeout(function() { + test.equal(location.pathname, simulatedBasePath + '/' + rand); + setBasePath(previousBasePath); + done(); + }, 100); +}); + +Tinytest.addAsync( +'Client - Router - base path - route action called', +function(test, done) { + var simulatedBasePath = '/flow'; + var previousBasePath = FlowRouter._basePath; + var rand = Random.id(); + FlowRouter.route('/' + rand, { + action: function() { + setBasePath(previousBasePath); + done(); + } + }); + + setBasePath(simulatedBasePath); + FlowRouter.go('/' + rand); +}); + + +function setBasePath(path) { + FlowRouter._initialized = false; + FlowRouter._basePath = path; + FlowRouter.initialize(); +} + function bind(obj, method) { return function() { obj[method].apply(obj, arguments); From bbba4e3d92d9d2911b76f610e3649adc3d40ad28 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Sat, 3 Oct 2015 15:24:37 +0200 Subject: [PATCH 2/5] Set the pagejs configuration in the correct order We need to set the `._page.base` path before calling the `_page` routing function. This fixes the first page routing when a page prefix is set. --- client/router.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/router.js b/client/router.js index e461ce5..a485540 100644 --- a/client/router.js +++ b/client/router.js @@ -369,12 +369,11 @@ Router.prototype.initialize = function(options) { // in unpredicatable manner. See #168 // this is the default behaviour and we need keep it like that // we are doing a hack. see .path() + this._page.base(this._basePath); this._page({ - basePath: this._basePath, decodeURLComponents: true, hashbang: !!options.hashbang }); - this._page.base(this._basePath); this._initialized = true; }; From e0304165fc21ed84f6a9a12348ab761f24f7479f Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Sat, 3 Oct 2015 15:28:41 +0200 Subject: [PATCH 3/5] Handle the prefixed paths in the `path` method --- client/router.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/router.js b/client/router.js index a485540..4276a87 100644 --- a/client/router.js +++ b/client/router.js @@ -137,9 +137,16 @@ Router.prototype.path = function(pathDef, fields, queryParams) { pathDef = this._routesMap[pathDef].pathDef; } + var path = ""; + + // Prefix the path with the router global prefix + if (this._basePath) { + path += this._basePath + "/"; + } + fields = fields || {}; var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g; - var path = pathDef.replace(regExp, function(key) { + path += pathDef.replace(regExp, function(key) { var firstRegexpChar = key.indexOf("("); // get the content behind : and (\\d+/) key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined); @@ -153,7 +160,8 @@ Router.prototype.path = function(pathDef, fields, queryParams) { return encodeURIComponent(encodeURIComponent(fields[key] || "")); }); - path = path.replace(/\/\/+/g, "/"); // Replace multiple slashes with single slash + // Replace multiple slashes with single slash + path = path.replace(/\/\/+/g, "/"); // remove trailing slash // but keep the root slash if it's the only one From b21b8c6e66525e6e9e9c819ce992324d0ce851d0 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Sat, 3 Oct 2015 15:56:22 +0200 Subject: [PATCH 4/5] Add a test for the prefixed path generation --- client/router.js | 2 +- test/client/router.core.spec.js | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/client/router.js b/client/router.js index 4276a87..55cef30 100644 --- a/client/router.js +++ b/client/router.js @@ -141,7 +141,7 @@ Router.prototype.path = function(pathDef, fields, queryParams) { // Prefix the path with the router global prefix if (this._basePath) { - path += this._basePath + "/"; + path += "/" + this._basePath + "/"; } fields = fields || {}; diff --git a/test/client/router.core.spec.js b/test/client/router.core.spec.js index 22ef358..160c911 100644 --- a/test/client/router.core.spec.js +++ b/test/client/router.core.spec.js @@ -574,7 +574,6 @@ Tinytest.addAsync( 'Client - Router - base path - url updated', function(test, done) { var simulatedBasePath = '/flow'; - var previousBasePath = FlowRouter._basePath; var rand = Random.id(); FlowRouter.route('/' + rand, { action: function() {} }); @@ -582,7 +581,7 @@ function(test, done) { FlowRouter.go('/' + rand); setTimeout(function() { test.equal(location.pathname, simulatedBasePath + '/' + rand); - setBasePath(previousBasePath); + resetBasePath(); done(); }, 100); }); @@ -591,11 +590,10 @@ Tinytest.addAsync( 'Client - Router - base path - route action called', function(test, done) { var simulatedBasePath = '/flow'; - var previousBasePath = FlowRouter._basePath; var rand = Random.id(); FlowRouter.route('/' + rand, { action: function() { - setBasePath(previousBasePath); + resetBasePath(); done(); } }); @@ -604,6 +602,17 @@ function(test, done) { FlowRouter.go('/' + rand); }); +Tinytest.add( +'Client - Router - base path - path generation', +function(test, done) { + _.each(['/flow', '/flow/', 'flow/', 'flow'], function(simulatedBasePath) { + var rand = Random.id(); + setBasePath(simulatedBasePath); + test.equal(FlowRouter.path('/' + rand), '/flow/' + rand); + }); + resetBasePath(); +}); + function setBasePath(path) { FlowRouter._initialized = false; @@ -611,6 +620,11 @@ function setBasePath(path) { FlowRouter.initialize(); } +var defaultBasePath = FlowRouter._basePath; +function resetBasePath() { + setBasePath(defaultBasePath); +} + function bind(obj, method) { return function() { obj[method].apply(obj, arguments); From cafa42e097f36c59cfc273f2af67b00180890256 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Sat, 3 Oct 2015 16:09:30 +0200 Subject: [PATCH 5/5] Document prefixed paths --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0253935..ce65545 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ It exposes a great API for changing the URL and reactively getting data from the * [Subscription Management](#subscription-management) * [IE9 Support](#ie9-support) * [Hashbang URLs](#hashbang-urls) +* [Prefixed paths](#prefixed-paths) * [Addons](#addons) * [Difference with Iron Router](#difference-with-iron-router) * [Migrating into 2.0](#migrating-into-20) @@ -637,6 +638,9 @@ WhenEverYourAppIsReady(function() { }); ~~~ +## Prefixed paths + +In cases you wish to run multiple web application on the same domain name, you’ll probably want to serve your particular meteor application under a sub-path (eg `example.com/myapp`). In this case simply include the path prefix in the meteor `ROOT_URL` environment variable and FlowRouter will handle it transparently without any additional configuration. ## Addons