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;