Commit 49690280 authored by Matija Obreza's avatar Matija Obreza
Browse files

Added support to be hosted on a /virtua-l/path

- Language detection updated
- Using <base href="/virtua-l/path" /> for SSR, development uses "/"
parent fda409bb
......@@ -17,6 +17,7 @@ module.exports = {
output: {
filename: '[name].js',
chunkFilename: '[name].js',
publicPath: '',
path: path.join(process.cwd(), 'target/app/server')
},
......
......@@ -102,7 +102,7 @@ module.exports = {
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
path: path.join(process.cwd(), 'target/app/assets'),
publicPath: '/'
publicPath: ''
},
resolve: {
......@@ -232,7 +232,7 @@ module.exports = {
template: './entrypoints/index.html',
chunksSortMode: sortedChunks(['vendor', 'common', 'catalog']),
chunks: ['vendor', 'common', 'catalog'],
favicon: './favicon.ico'
favicon: 'favicon.ico'
}),
// Defer/Async scripts
......@@ -251,7 +251,7 @@ module.exports = {
template: './server/ssr-template.html',
chunksSortMode: sortedChunks(['vendor', 'common', 'catalog']),
chunks: ['vendor', 'common', 'catalog'],
favicon: './favicon.ico'
favicon: 'favicon.ico'
}),
......
......@@ -26,8 +26,11 @@ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const initialLanguage = window.initialLanguage;
const initialI18nStore = window.initialI18nStore;
const detectedLang = initialLanguage ? initialLanguage : detectLocaleFromPath(window.location.pathname, 0);
const historyOptions = { basename: detectedLang !== 'en' ? `/${detectedLang}` : '' };
// document.baseURI is full URI, we take everything but the trailing slash (for "/" it is "", for "/aa/" or "/aa" it is "/aa")
const virtualPath = document.baseURI.replace(/^(https?:\/\/[^\/]+)?(.*)\/$/, '$2');
const detectedLang = initialLanguage ? initialLanguage : detectLocaleFromPath(virtualPath, window.location.pathname, 0);
const historyOptions = { basename: detectedLang !== 'en' ? `${virtualPath}/${detectedLang}` : `${virtualPath}` };
const history = createHistory(historyOptions);
const initialState = __PRELOADED_STATE__ === undefined ? {} : __PRELOADED_STATE__;
......
......@@ -3,6 +3,7 @@
<head>
<title>Genesys PGR</title>
<base href="/" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes" />
<meta name="author" content="Genesys Team, helpdesk@genesys-pgr.org" />
......
......@@ -8,8 +8,11 @@ const LanguageDetector = require('i18next-browser-languagedetector');
const lngDetector = new LanguageDetector();
lngDetector.addDetector(langDetector);
// copied from client.tsx
const virtualPath = document.baseURI.replace(/^(https?:\/\/[^\/]+)?(.*)\/$/, '$2');
const backend = {
loadPath: '/locales/{{lng}}/{{ns}}.json',
loadPath: `${virtualPath}/locales/{{lng}}/{{ns}}.json`,
// addPath: '/locales/{{lng}}/{{ns}}.missing.json',
};
......@@ -20,6 +23,12 @@ optionsBase[backendKey] = backend;
const i18nClient = i18n
.use(XHR)
.use(lngDetector)
.init(optionsBase);
.init({
detection: {
order: ['catalogLangDetector'],
lookupFromPathIndex: 0,
},
...optionsBase
});
export default i18nClient;
......@@ -17,6 +17,12 @@ optionsBase[backendKey] = backend;
const i18nServer = i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init(optionsBase);
.init({
detection: {
order: ['path'],
lookupFromPathIndex: 0,
},
...optionsBase
});
export default i18nServer;
......@@ -6,9 +6,12 @@ export default {
lookup: function lookup(options) {
let found;
if (typeof window !== 'undefined') {
found = detectLocaleFromPath(window.location.pathname, options.lookupFromPathIndex);
// copied from client.tsx
const virtualPath = document.baseURI.replace(/^(https?:\/\/[^\/]+)?(.*)\/$/, '$2');
found = detectLocaleFromPath(virtualPath, window.location.pathname, options.lookupFromPathIndex);
}
return found;
},
};
const optionsBase = {
detection: {
order: ['catalogLangDetector'],
lookupFromPathIndex: 0,
},
fallbackLng: 'en',
preload: ['en'],
load: 'languageOnly', // we only provide en, de -> no region specific locals like en-US, de-DE
......
......@@ -4136,8 +4136,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
......@@ -4158,14 +4157,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
......@@ -4180,20 +4177,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
......@@ -4310,8 +4304,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
......@@ -4323,7 +4316,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
......@@ -4338,7 +4330,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
......@@ -4346,8 +4337,7 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
......@@ -4371,7 +4361,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
......@@ -4452,8 +4441,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
......@@ -4586,7 +4574,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
......@@ -4606,7 +4593,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
......
......@@ -3,7 +3,7 @@ import * as minimist from 'minimist';
// console.log(process.argv);
const argv = minimist(process.argv.slice(2), {
string: [
'--genesys-url', '--genesys-client-id', '--genesys-client-secret',
'--api-url', '--client-id', '--client-secret', '--frontend-path',
],
});
console.dir(argv);
......@@ -13,14 +13,19 @@ console.dir(argv);
*/
const config = {
// Timeout (ms) for proxied calls to the API
frontendPath: argv['frontend-path'] || '',
// Backend
apiTimeout: +(argv['api-timeout'] || 2000),
// Genesys PGR
genesysUrl: argv['genesys-url'] || 'localhost:8080',
genesysClientId: argv['genesys-client-id'] || 'my-trusted-client',
genesysClientSecret: argv['genesys-client-secret'] || 'my-secret-client',
apiUrl: argv['api-url'] || 'localhost:8080',
clientId: argv['client-id'] || 'defaultclient@localhost',
clientSecret: argv['client-secret'] || 'changeme',
};
if (config.frontendPath.endsWith('/')) {
console.log(`FRONTEND_PATH "${config.frontendPath}" must not end with trailing /`);
config.frontendPath = config.frontendPath.substring(0, config.frontendPath.lastIndexOf('/'));
}
console.log('Catalog config', config);
export default config;
import languages from 'data/Languages';
export default function detectLocaleFromPath(path, index) {
export default function detectLocaleFromPath(virtualPath: string, path: string, index: number) {
if (virtualPath && path.startsWith(virtualPath)) {
path = path.substring(virtualPath.length);
}
let found = 'en';
const language = path.match(/\/([a-zA-Z-]*)/g);
if (language instanceof Array) {
if (typeof index === 'number') {
const foundLang = language[index].replace('/', '');
languages.some((item, i, arr) => {
if (item.short === foundLang) {
found = foundLang;
return true;
}
});
}
const foundLang = language[index].replace('/', '');
languages.some((item, i, arr) => {
if (item.short === foundLang) {
found = foundLang;
return true;
}
});
}
return found;
}
import * as proxy from 'express-http-proxy';
import config from '../config';
const genesysProxy = proxy(config.genesysUrl, {
const genesysProxy = proxy(config.apiUrl, {
parseReqBody: false,
timeout: config.apiTimeout * 2, // double normal timeout
timeout: config.apiTimeout,
filter: (req, res) => {
if (req.url.startsWith('/oauth/') || req.url.startsWith('/token') || req.url.startsWith('/google')) {
......@@ -16,11 +16,11 @@ const genesysProxy = proxy(config.genesysUrl, {
},
proxyReqPathResolver: (req) => {
let path = req.url;
let path = req.url; // .replace(config.frontendPath, '');
if (path.startsWith('/oauth/token')) {
const grantType = req.query.grant_type;
if (grantType === 'client_credentials' || grantType === 'password') {
path = `${path}&client_id=${config.genesysClientId}&client_secret=${config.genesysClientSecret}`;
path = `${path}&client_id=${config.clientId}&client_secret=${config.clientSecret}`;
// remove all headers from request
req.headers = {};
}
......@@ -34,7 +34,7 @@ const genesysProxy = proxy(config.genesysUrl, {
delete req.headers.cookie;
}
console.log(`HTTP proxy to ${config.genesysUrl}${path}`);
console.log(`HTTP proxy to ${config.apiUrl}${path}`);
return path;
},
});
......
......@@ -24,6 +24,7 @@ import renderRoutes from 'ui/renderRoutes';
import fetchComponentData from './fetchComponentData';
import detectLocaleFromPath from './detectLocaleFromPath';
import getDir from './detectDirection';
import config from '../config';
const prerenderer = (html) => (req, res) => {
console.log('Init prerenderer, request url:', req.url);
......@@ -31,12 +32,13 @@ const prerenderer = (html) => (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
const store = createStore(rootReducer, {} as any, applyMiddleware(thunk, routerMiddleware(createMemoryHistory)));
console.log(`Processing request`, req._parsedOriginalUrl);
const pathname = req._parsedOriginalUrl.pathname;
const search = req._parsedOriginalUrl.search;
const context = {};
function setLocale() {
const locale = detectLocaleFromPath(pathname, 0);
const locale = detectLocaleFromPath(config.frontendPath, pathname, 0);
console.log('Detected locale for SSR is', locale);
req.i18n.changeLanguage(locale);
}
......@@ -47,17 +49,22 @@ const prerenderer = (html) => (req, res) => {
const generateClassName = createGenerateClassName();
const initialLanguage = req.i18n.language;
const initialI18nStore = {};
req.i18n.languages.forEach((l) => {
initialI18nStore[l] = req.i18n.services.resourceStore.data[l];
});
const basename = initialLanguage !== 'en' ? `/${initialLanguage}` : '';
initialI18nStore[initialLanguage] = req.i18n.services.resourceStore.data[initialLanguage];
if (initialLanguage !== 'en') {
// So that we have missing translations
initialI18nStore['en'] = req.i18n.services.resourceStore.data['en'];
}
const basename = initialLanguage !== 'en' ? `${config.frontendPath}/${initialLanguage}` : `${config.frontendPath}`;
const pathWithoutLang = req.url.substr(initialLanguage !== 'en' ? 3 : 0, req.url.length);
console.log(`<StaticRouter location="${pathWithoutLang}" basename="${basename}"`);
const InitialView = (
<ReduxProvider store={ store }>
<JssProvider generateClassName={ generateClassName } registry={ sheets }>
<MuiThemeProvider theme={ theme } sheetsManager={ new Map() }>
<I18nextProvider i18n={ req.i18n }>
<StaticRouter location={ req.url } context={ context } basename={ basename }>
<StaticRouter location={ pathWithoutLang } context={ context } basename={ basename }>
{ renderRoutes(routes) }
</StaticRouter>
</I18nextProvider>
......@@ -76,52 +83,57 @@ const prerenderer = (html) => (req, res) => {
console.log('initialState:', initialState);
}
return html
.replace('SERVER_RENDERED_CSS', sheets.toString())
.replace('SERVER_RENDERED_STATE', serialize(initialState, {isJSON: true}))
.replace('SERVER_RENDERED_HTML', componentHTML)
.replace('SERVER_RENDERED_TITLE', titleState)
.replace('INITIAL_I18N_STORE', serialize(initialI18nStore, {isJSON: true}))
.replace('SERVER_DETECTED_LANGUAGE', serialize(initialLanguage, {isJSON: false}))
.replace('HTML_DIR', serialize(getDir(initialLanguage), {isJSON: false}))
.replace('HTML_LANG', serialize(initialLanguage, {isJSON: false}));
return html.replace(/SERVER_RENDERED_(CSS|STATE|HTML|TITLE|I18NSTORE|DIR|LANG)/g, (match, x) => {
console.log(`Injecting ${match}`);
switch (match) {
case 'SERVER_RENDERED_CSS': return sheets.toString();
case 'SERVER_RENDERED_STATE': return serialize(initialState, {isJSON: true});
case 'SERVER_RENDERED_HTML': return componentHTML;
case 'SERVER_RENDERED_TITLE': return titleState;
case 'SERVER_RENDERED_I18NSTORE': return serialize(initialI18nStore, {isJSON: true});
case 'SERVER_RENDERED_DIR': return serialize(getDir(initialLanguage), {isJSON: false});
case 'SERVER_RENDERED_LANG': return serialize(initialLanguage, {isJSON: false});
default: console.log(`Unrecognized variable in ssr-template.html: ${match}`); return '';
}
});
}
setLocale();
// checkAuthToken(req, res, store.dispatch).then(() => {
const language = req.i18n.language;
const pathWithoutLang = pathname.substr(language !== 'en' ? 3 : 0, pathname.length);
const branch = matchRoutes(routes, pathWithoutLang);
fetchComponentData(store.dispatch, branch, search)
.then(() => { return renderView();
}).then((html) => {
const serverRenderTime = `${Date.now() - startTime}ms`;
console.log('Server render time:', startTime, Date.now(), serverRenderTime);
const r = html.replace('__SERVER_RENDER_TIME__', serverRenderTime);
const keyStatus = 'status';
const keyUrl = 'url';
const status = context[keyStatus];
const url = context[keyUrl];
if (status && status === 404) {
res.status(404);
}
if (url) {
if (url === '/login') {
res.status(401);
} else {
res.status(302);
}
const language = req.i18n.language;
const pathWithoutLang = pathname.substr(language !== 'en' ? 3 : 0, pathname.length);
console.log(`Rendering ${pathWithoutLang} for ${pathname}`);
const branch = matchRoutes(routes, pathWithoutLang);
fetchComponentData(store.dispatch, branch, search)
.then(() => { return renderView();
}).then((html) => {
const serverRenderTime = `${Date.now() - startTime}ms`;
console.log('Server render time:', startTime, Date.now(), serverRenderTime);
const r = html.replace('__SERVER_RENDER_TIME__', serverRenderTime);
const keyStatus = 'status';
const keyUrl = 'url';
const status = context[keyStatus];
const url = context[keyUrl];
if (status && status === 404) {
res.status(404);
}
if (url) {
if (url === '/login') {
res.status(401);
} else {
res.status(302);
}
}
return res.set('Content-Type', 'text/html').send(r);
}).catch((err) => {
console.error('Error:', err);
res.status(500).end(err.message);
});
return res.set('Content-Type', 'text/html').send(r);
}).catch((err) => {
console.error('Error:', err);
res.status(500).end(err.message);
});
// });
};
......
......@@ -7,6 +7,7 @@ import * as cookieParser from 'cookie-parser';
import * as compression from 'compression';
import {readFileSync} from 'fs';
import config from './config';
import prerenderer from './middleware/prerenderer';
import genesysProxy from './middleware/genesysProxy';
import robots from './robots';
......@@ -16,10 +17,8 @@ const app = express();
// This reads the ssr-template.html compiled to ssr-compiled.html by webpack
console.log(`Reading SSR template from ${path.join('../assets', 'ssr-compiled.html')}`);
const html = readFileSync(path.join('../assets', 'ssr-compiled.html'), {encoding: 'utf8'});
const html = readFileSync(path.join('../assets', 'ssr-compiled.html'), {encoding: 'utf8'}).replace('FRONTEND_PATH', `${config.frontendPath}/`);
// Register i18n
app.use(i18nextMiddleware.handle(i18nServer));
// Log all requests
app.use((req, res, next) => {
console.log('Incoming request, url:', req.url);
......@@ -29,14 +28,14 @@ app.use((req, res, next) => {
app.get('/en/*', (req, res) => {
const oldPath = req.url;
const redirectPath = oldPath.substr(3, oldPath.length);
res.redirect(redirectPath);
res.redirect(`${config.frontendPath}${redirectPath}`);
});
// Enable compression
app.use(compression());
// Proxy all requests starting with /proxy
app.use('/api/genesys', genesysProxy);
// robots.txt
app.get('/robots.txt', robots);
// Proxy all requests starting with /proxy
app.use('/proxy', genesysProxy);
// Serve static resources (this should be the only thing publicly accessible)
app.use(express.static(path.join('../assets'), {
etag: true,
......@@ -45,7 +44,18 @@ app.use(express.static(path.join('../assets'), {
redirect: false,
immutable: true,
}));
// Serve /locales
app.use('/locales/', express.static(path.join('../assets/locales'), {
etag: true,
maxAge: '7d',
index: false,
redirect: false,
immutable: true,
}));
// Register i18n
app.use(i18nextMiddleware.handle(i18nServer));
// Parse cookies
app.use(cookieParser());
// Relay requests to React
app.use(prerenderer(html));
......
<!DOCTYPE html>
<html lang=HTML_LANG dir=HTML_DIR >
<html lang=SERVER_RENDERED_LANG dir=SERVER_RENDERED_DIR>
<head>
<title>SERVER_RENDERED_TITLE</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes" />
<meta name="author" content="Genesys Team, helpdesk@genesys-pgr.org" />
<style type="text/css" id="server-side-styles">
SERVER_RENDERED_CSS
</style>
</head>
<head>
<title>SERVER_RENDERED_TITLE</title>
<base href="FRONTEND_PATH" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes" />
<meta name="author" content="Genesys Team, helpdesk@genesys-pgr.org" />
<style type="text/css" id="server-side-styles">
SERVER_RENDERED_CSS
</style>
</head>
<body>
<!--[if lt IE 7]>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<![endif]-->
<div id="the-app">SERVER_RENDERED_HTML</div>
<footer class="footer">
<div class="footer-container">
<div class="footer-link">
Processing this page took __SERVER_RENDER_TIME__.
</div>
<div id="the-app">SERVER_RENDERED_HTML</div>