diff --git a/config/string.js b/config/string.js
index ffe091e..defe320 100644
--- a/config/string.js
+++ b/config/string.js
@@ -36,6 +36,11 @@ function replaceString() {
replace: normalizeUrl(voltranConfig.routing.requestConfigs),
flags: 'g'
},
+ {
+ search: '__V_PREVIEW_PAGES__',
+ replace: normalizeUrl(voltranConfig.routing.previewPages),
+ flags: 'g'
+ },
{
search: '@voltran/core',
replace: normalizeUrl(path.resolve(__dirname, '../src/index')),
diff --git a/package.json b/package.json
index b09df9b..671af7e 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"classnames": "2.2.6",
"clean-webpack-plugin": "1.0.0",
"cli-color": "^2.0.0",
+ "colors": "^1.4.0",
"compose-middleware": "5.0.0",
"compression": "^1.7.4",
"cookie-parser": "1.4.3",
diff --git a/src/index.js b/src/index.js
index d534a79..d217a12 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,6 @@
-import withBaseComponent from './universal/partials/withBaseComponent';
+import voltran from './universal/partials/withBaseComponent';
import { SERVICES } from './universal/utils/constants';
+import { ClientApiManager, ServerApiManager } from './universal/core/api';
-export default {
- withBaseComponent,
- SERVICES
-};
+export default voltran;
+export { SERVICES, ClientApiManager, ServerApiManager };
diff --git a/src/renderMultiple.js b/src/renderMultiple.js
index 01cb05f..c821dc0 100644
--- a/src/renderMultiple.js
+++ b/src/renderMultiple.js
@@ -4,11 +4,31 @@ import { matchUrlInRouteConfigs } from './universal/core/route/routeUtils';
import Component from './universal/model/Component';
import Renderer from './universal/model/Renderer';
import Preview from './universal/components/Preview';
-import { isRequestDispatcher, isPreview, isWithoutHTML } from './universal/service/RenderService';
+import {
+ isRequestDispatcher,
+ isPreview,
+ isWithoutHTML,
+ isWithoutState,
+ getPreviewLayout
+} from './universal/service/RenderService';
import metrics from './metrics';
import { HTTP_STATUS_CODES } from './universal/utils/constants';
import logger from './universal/utils/logger';
+const previewPages = require('__V_PREVIEW_PAGES__');
+
+const getRenderOptions = req => {
+ const isPreviewValue = isPreview(req.query) || false;
+ const isWithoutHTMLValue = isWithoutHTML(req.query) || false;
+ const isWithoutStateValue = isWithoutState(req.query) || false;
+
+ return {
+ isPreview: isPreviewValue,
+ isWithoutHTML: isWithoutHTMLValue,
+ isWithoutState: isWithoutStateValue
+ };
+};
+
function getRenderer(name, req) {
const { query, cookies, url, headers, params } = req;
const path = `/${params?.path || ''}`;
@@ -16,6 +36,7 @@ function getRenderer(name, req) {
const componentPath = Component.getComponentPath(name);
const routeInfo = matchUrlInRouteConfigs(componentPath);
+ const renderOptions = getRenderOptions(req);
if (routeInfo) {
const urlWithPath = url.replace('/', path);
@@ -26,6 +47,7 @@ function getRenderer(name, req) {
cookies,
url: urlWithPath,
userAgent,
+ ...renderOptions
};
if (Component.isExist(componentPath)) {
@@ -177,11 +199,21 @@ async function getResponses(renderers) {
return responses;
}
-async function getPreview(responses, requestCount) {
- return Preview(
- [...Object.keys(responses).map(name => responses[name].fullHtml)].join('\n'),
- `${requestCount} request!`
- );
+async function getPreview(responses, requestCount, req) {
+ const layoutName = getPreviewLayout(req.query);
+ const { layouts } = previewPages.default;
+ let PreviewFile = Preview;
+
+ if (layouts[layoutName]) {
+ PreviewFile = layouts[layoutName];
+ }
+
+ const content = Object.keys(responses).map(name => {
+ const componentName = responses?.[name]?.activeComponent?.componentName ?? '';
+ return getLayoutWithClass(componentName, responses[name].fullHtml);
+ });
+
+ return PreviewFile([...content].join('\n'), `${requestCount} request!`);
}
const DEFAULT_PARTIALS = ['RequestDispatcher'];
@@ -199,6 +231,17 @@ export const getPartials = req => {
return partials;
};
+function cr(condition, ok, cancel) {
+ return condition ? ok : cancel || '';
+}
+
+const getLayoutWithClass = (name, html, id = '', style = null) => {
+ const idAttr = cr(id !== '', `id=${id}`);
+ const styleAttr = cr(style !== null, `style=${style}`);
+
+ return `
${html}
`;
+};
+
const renderMultiple = async (req, res) => {
const partials = getPartials(req);
@@ -228,7 +271,7 @@ const renderMultiple = async (req, res) => {
const responses = await getResponses(renderers);
if (isPreview(req.query)) {
- const preview = await getPreview(responses, requestCount);
+ const preview = await getPreview(responses, requestCount, req);
res.html(preview);
} else {
res.json(responses);
diff --git a/src/universal/core/api/index.js b/src/universal/core/api/index.js
new file mode 100644
index 0000000..2990a60
--- /dev/null
+++ b/src/universal/core/api/index.js
@@ -0,0 +1,2 @@
+export { default as ClientApiManager } from './ClientApiManagerCache';
+export { default as ServerApiManager } from './ServerApiManagerCache';
diff --git a/src/universal/model/Renderer.js b/src/universal/model/Renderer.js
index 4bfcc2e..1809867 100644
--- a/src/universal/model/Renderer.js
+++ b/src/universal/model/Renderer.js
@@ -1,6 +1,13 @@
-import ServerApiManagerCache from '../core/api/ServerApiManagerCache';
+import omit from 'lodash/omit';
import { isPreview, renderComponent, renderLinksAndScripts } from '../service/RenderService';
+const blacklistOutput = [
+ 'componentName',
+ 'fullWidth',
+ 'isMobileComponent',
+ 'isPreviewQuery',
+ 'responseOptions'
+];
export default class Renderer {
constructor(component, context) {
this.component = component;
@@ -13,44 +20,42 @@ export default class Renderer {
this.isPredefinedInitialStateSupported() &&
(process.env.BROWSER || (!process.env.BROWSER && !this.context.isWithoutState))
) {
- this.servicesMap = this.getServicesMap();
+ this.servicesMap = this.getServicesWithMultiple();
this.winnerMap = {};
}
}
setInitialState(prepareInitialStateArgs) {
this.initialState = {
- data: this.component.object.prepareInitialState(...prepareInitialStateArgs)
+ data: this.component.object.getInitialStateWithMultiple(...prepareInitialStateArgs)
};
}
isPredefinedInitialStateSupported() {
- return this.component.object.getServicesMap && this.component.object.prepareInitialState;
- }
-
- getServicesMap() {
- const services = this.component.object.services.map(
- serviceName => ServerApiManagerCache[serviceName]
+ return (
+ this.component.object.getServicesWithMultiple &&
+ this.component.object.getInitialStateWithMultiple
);
+ }
- const params = [...services, this.context];
- return this.component.object.getServicesMap(...params);
+ getServicesWithMultiple() {
+ const options = { isServer: false };
+ return this.component.object.getServicesWithMultiple(this.context, options);
}
render() {
return new Promise(resolve => {
renderComponent(this.component, this.context, this.initialState).then(response => {
- const { output, links, scripts, activeComponent, seoState, fullHtml } = response;
+ const { output, links, fullHtml, ...rest } = response;
+ const otherParams = omit(rest, blacklistOutput);
const html = renderLinksAndScripts(output, '', '');
resolve({
key: this.component.name,
value: {
html,
- scripts,
style: links,
- activeComponent,
- seoState,
+ ...otherParams,
...(isPreview(this.context?.query) && { fullHtml })
},
id: this.component.id
diff --git a/src/universal/partials/Welcome/PartialList.js b/src/universal/partials/Welcome/PartialList.js
index e225d26..b406108 100644
--- a/src/universal/partials/Welcome/PartialList.js
+++ b/src/universal/partials/Welcome/PartialList.js
@@ -12,7 +12,7 @@ const Welcome = () => {
const { live = [], dev = [], page = [] } = groupBy(partials, item => item.status);
const renderItem = item => (
-
+
{item.name}
{item.url}
@@ -26,12 +26,24 @@ const Welcome = () => {
);
return (
- Live
- {live.map(item => renderItem(item))}
- Pages
- {page.map(item => renderItem(item))}
- Development
- {dev.map(item => renderItem(item))}
+ {live.length > 0 && (
+ <>
+ Live
+ {live.map(item => renderItem(item))}
+ >
+ )}
+ {page.length > 0 && (
+ <>
+ Pages
+ {page.map(item => renderItem(item))}
+ >
+ )}
+ {dev.length > 0 && (
+ <>
+ Development
+ {dev.map(item => renderItem(item))}
+ >
+ )}
);
};
diff --git a/src/universal/partials/Welcome/partials.js b/src/universal/partials/Welcome/partials.js
index de47701..0e648ee 100644
--- a/src/universal/partials/Welcome/partials.js
+++ b/src/universal/partials/Welcome/partials.js
@@ -1,5 +1,7 @@
import components from '../../core/route/components';
+const previewPages = require('__V_PREVIEW_PAGES__');
+
const partials = [];
Object.keys(components).forEach(path => {
@@ -10,5 +12,6 @@ Object.keys(components).forEach(path => {
status: info.status
});
});
+partials.push(...previewPages.default.pages);
export default partials;
diff --git a/src/universal/partials/Welcome/styled.js b/src/universal/partials/Welcome/styled.js
index 0e16937..020e4c9 100644
--- a/src/universal/partials/Welcome/styled.js
+++ b/src/universal/partials/Welcome/styled.js
@@ -2,12 +2,15 @@ import styled from 'styled-components';
const STATUS_COLOR = {
live: '#8dc63f',
- dev: '#FF6000'
+ dev: '#FF6000',
+ page: '#00abff'
};
+
export const List = styled.ul`
list-style: none;
margin: 0;
padding: 0;
+ margin-bottom: 20px;
`;
export const HeaderName = styled.div`
@@ -18,19 +21,41 @@ export const HeaderName = styled.div`
export const ListItem = styled.li`
padding: 20px;
- border-radius: 2px;
- background: white;
- box-shadow: 0 2px 1px rgba(170, 170, 170, 0.25);
- position: relative;
display: inline-block;
vertical-align: top;
height: 120px;
width: 320px;
margin: 10px;
cursor: pointer;
+ border-radius: 20px;
+ border: 1px solid ${({ status }) => (status && STATUS_COLOR[status]) || '#8dc63f'};
+
+ position: relative;
+ background-color: #fff;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ -webkit-transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
+ transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
+
+ :after {
+ content: '';
+ border-radius: 20px;
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ -webkit-transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
+ transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1);
+ }
&:hover {
- background: #efefef;
+ transform: scale(1.02, 1.02);
+ :after {
+ opacity: 1;
+ }
}
@media screen and (max-width: 600px) {
@@ -58,7 +83,7 @@ export const Link = styled.a`
`;
export const Name = styled.span`
- font-weight: 400;
+ font-weight: 800;
display: block;
max-width: 80%;
font-size: 16px;
diff --git a/src/universal/service/RenderService.js b/src/universal/service/RenderService.js
index 15d8391..0303b21 100644
--- a/src/universal/service/RenderService.js
+++ b/src/universal/service/RenderService.js
@@ -2,42 +2,13 @@ import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { StaticRouter } from 'react-router';
+
import ConnectedApp from '../components/App';
import Html from '../components/Html';
import PureHtml, { generateLinks, generateScripts } from '../components/PureHtml';
-import ServerApiManagerCache from '../core/api/ServerApiManagerCache';
import createBaseRenderHtmlProps from '../utils/baseRenderHtml';
import { guid } from '../utils/helper';
-
-const getStates = async (component, context, predefinedInitialState) => {
- const initialState = predefinedInitialState || { data: {} };
- let subComponentFiles = [];
- let seoState = {};
- let responseOptions = {};
-
- if (context.isWithoutState) {
- return { initialState, seoState, subComponentFiles, responseOptions };
- }
-
- if (!predefinedInitialState && component?.getInitialState) {
- const services = component.services.map(serviceName => ServerApiManagerCache[serviceName]);
- initialState.data = await component.getInitialState(...[...services, context]);
- }
-
- if (component?.getSeoState) {
- seoState = component.getSeoState(initialState.data);
- }
-
- if (initialState.data.subComponentFiles) {
- subComponentFiles = initialState.data.subComponentFiles;
- }
-
- if (initialState.data.responseOptions) {
- responseOptions = initialState.data.responseOptions;
- }
-
- return { initialState, seoState, subComponentFiles, responseOptions };
-};
+import getStates from './getStates';
const renderLinksAndScripts = (html, links, scripts) => {
return html
@@ -86,6 +57,14 @@ const isPreview = query => {
return query.preview === '';
};
+const getPreviewLayout = query => {
+ if (query?.previewLayout) {
+ return query?.previewLayout;
+ }
+
+ return '';
+};
+
const isWithoutState = query => {
return query.withoutState === '';
};
@@ -95,7 +74,7 @@ const isRequestDispatcher = query => {
};
const renderComponent = async (component, context, predefinedInitialState = null) => {
- const { initialState, seoState, subComponentFiles, responseOptions } = await getStates(
+ const { initialState, subComponentFiles, ...restStates } = await getStates(
component.object,
context,
predefinedInitialState
@@ -116,11 +95,10 @@ const renderComponent = async (component, context, predefinedInitialState = null
scripts,
activeComponent,
componentName: component.name,
- seoState,
fullWidth: component.fullWidth,
isMobileComponent: component.isMobileComponent,
isPreviewQuery: component.isPreviewQuery,
- responseOptions
+ ...restStates
};
};
@@ -130,6 +108,7 @@ export {
getStates,
isWithoutHTML,
isPreview,
+ getPreviewLayout,
isRequestDispatcher,
isWithoutState,
renderComponent
diff --git a/src/universal/service/getStates.js b/src/universal/service/getStates.js
new file mode 100644
index 0000000..f1297d4
--- /dev/null
+++ b/src/universal/service/getStates.js
@@ -0,0 +1,96 @@
+import omit from 'lodash/omit';
+import camelCase from 'lodash/camelCase';
+
+const blacklistFunctionName = [
+ 'getServerSideProps',
+ 'getServicesWithMultiple',
+ 'getInitialStateWithMultiple',
+ 'setDependencies',
+ 'setSeoState',
+ 'setRedirection',
+ 'setPageData'
+];
+
+const getCustomSetters = (component, context, data) => {
+ const pattern = new RegExp(`^set`);
+ const functions = omit(component, blacklistFunctionName);
+ let result = {};
+
+ Object.entries(functions).forEach(entity => {
+ const [name, method] = entity;
+ const isValidName = pattern.test(name);
+ if (isValidName) {
+ const propertyName = camelCase(name.replace(pattern, ''));
+ let value = null;
+ if (typeof method === 'function') {
+ value = method(context, data);
+ } else {
+ value = method;
+ }
+
+ if (value) {
+ result = {
+ ...result,
+ [propertyName]: value
+ };
+ }
+ }
+ });
+
+ return result;
+};
+
+const getStates = async (component, context, predefinedInitialState) => {
+ const initialState = predefinedInitialState || { data: {} };
+ let subComponentFiles = [];
+ let seoState = {};
+ let responseOptions = {};
+ let dependencies = [];
+ let redirection = null;
+
+ if (component.setDependencies) {
+ dependencies = component.setDependencies(context);
+ }
+
+ if (context.isWithoutState) {
+ return { initialState, seoState, dependencies, subComponentFiles, responseOptions };
+ }
+
+ if (!predefinedInitialState && component.getServerSideProps) {
+ initialState.data = await component.getServerSideProps(context);
+ }
+
+ if (component?.setSeoState) {
+ seoState = component.setSeoState(initialState?.data) || {};
+ }
+
+ if (initialState?.data?.subComponentFiles) {
+ subComponentFiles = initialState?.data?.subComponentFiles || [];
+ }
+
+ if (initialState?.data?.responseOptions) {
+ responseOptions = initialState?.data?.responseOptions || {};
+ }
+
+ if (component.setRedirection) {
+ redirection = component.setRedirection(context, initialState.data);
+ }
+
+ if (component.setPageData) {
+ redirection = component.setPageData(context, initialState.data);
+ }
+
+ const setters = getCustomSetters(component, context, initialState.data);
+
+ return {
+ initialState,
+ seoState,
+ subComponentFiles,
+ responseOptions,
+ dependencies,
+ redirection,
+ ...setters
+ };
+};
+
+export default getStates;
diff --git a/src/universal/utils/logger.js b/src/universal/utils/logger.js
index 06b36ef..cd4878f 100644
--- a/src/universal/utils/logger.js
+++ b/src/universal/utils/logger.js
@@ -2,6 +2,7 @@
const application = 'voltran';
const currentThread = 'event-loop';
const sourceContext = 'app';
+const colors = require('colors');
const logger = {
formatter(level, message) {
@@ -13,7 +14,7 @@ const logger = {
return;
}
- console.log(this.formatter('INFO', message));
+ console.log(colors.blue(this.formatter('INFO', message)));
},
error(message) {
@@ -24,6 +25,10 @@ const logger = {
console.error(this.formatter('ERROR', message));
},
+ warning(message) {
+ console.warn(colors.yellow(message));
+ },
+
exception(exception, stack = true, requestPath = null) {
if (process.env.BROWSER && process.env.VOLTRAN_ENV === 'prod') {
return;