diff --git a/README.md b/README.md index 780cb03f..a9cbc4f0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Twitter -LambStatus is a status page system inspired by [StatusPage.io](https://www.statuspage.io/), built on AWS Lambda. +LambStatus is a serverless status page system inspired by [StatusPage.io](https://www.statuspage.io/). With a few clicks, You can build a status page like this: @@ -20,18 +20,19 @@ The demo pages are available: ## Goals of this project * Offers an open source and serverless status page system. -* Enables you to deploy and maintain the status page system at minimum effort. +* Offers a pay-as-you-go pricing approach like AWS. We estimate the system takes just *$1 to handle 30,000 visitors* ([see details](https://github.com/ks888/LambStatus/wiki/Cost-estimate)). +* Enables you to build and maintain the status page system at minimum effort. ## Why Serverless? Status page system is great with the Serverless architecture, because: -* It dramatically eases your pain caused by the scaling / availability issues. It is terrible if your service is down AND heavy traffic from stuck users stops your status page. -* It reduces your infrastructure cost. A status page usually gets very low traffic and occasionally huge traffic. You only pay for the traffic that you handle. +* It eases your pain caused by the scaling / availability issues. It is terrible if your service is down AND heavy traffic from stuck users stops your status page. +* It enables you to pay only for what you use. A status page only occasionally gets huge traffic. The system takes only $1 per 30,000 visitors and almost $0 if no visitors. Apart from the Serverless architecture, LambStatus enables you to: -* Easily build and update the system (by the power of the CloudFormation) +* Build and update the system with a few clicks (by the power of the CloudFormation) * Choose the AWS region different from your service's region. If both your service and its status page rely on the same region, [the region outage](https://aws.amazon.com/message/41926/) may stop both. ## Installation diff --git a/packages/frontend/build/cdn.js b/packages/frontend/build/cdn.js new file mode 100644 index 00000000..eab48d89 --- /dev/null +++ b/packages/frontend/build/cdn.js @@ -0,0 +1,29 @@ +import { spawnSync } from 'child_process' + +let depsCache + +const listDependencies = function () { + if (depsCache !== undefined) return depsCache + + const rawDeps = spawnSync('npm', ['list', '--json']) + depsCache = JSON.parse(rawDeps.stdout) + return depsCache +} + +const getVersion = function (cdnModule) { + let curr = listDependencies() + cdnModule.dependedBy.forEach(function (dep) { + curr = curr.dependencies[dep] + }) + return curr.dependencies[cdnModule.moduleName].version +} + +export const buildScriptURL = function (cdnModule) { + const version = getVersion(cdnModule) + return `https://cdnjs.cloudflare.com/ajax/libs/${cdnModule.libraryName}/${version}/${cdnModule.scriptPath}` +} + +export const buildCSSURL = function (cdnModule) { + const version = getVersion(cdnModule) + return `https://cdnjs.cloudflare.com/ajax/libs/${cdnModule.libraryName}/${version}/${cdnModule.cssPath}` +} diff --git a/packages/frontend/build/webpack.config.js b/packages/frontend/build/webpack.config.js index ca6277ed..ea0d861b 100644 --- a/packages/frontend/build/webpack.config.js +++ b/packages/frontend/build/webpack.config.js @@ -4,9 +4,36 @@ import HtmlWebpackPlugin from 'html-webpack-plugin' import ExtractTextPlugin from 'extract-text-webpack-plugin' import CopyWebpackPlugin from 'copy-webpack-plugin' import _debug from 'debug' +import { buildScriptURL, buildCSSURL } from './cdn' const debug = _debug('app:webpack:config') +// These modules are served from cdnjs. +// 'moduleName' and 'dependedBy' are used to resolve the module version. +// 'libraryName', 'cssPath' and 'scriptPath' are used to build the cdn url. +// If defined, 'external' is used to build the 'externals' option of webpack. +/* eslint-disable max-len */ +const modulesServedFromCDN = [ + {moduleName: 'c3', dependedBy: [], libraryName: 'c3', cssPath: 'c3.css', scriptPath: 'c3.js', external: 'c3'}, + {moduleName: 'd3', dependedBy: ['c3'], libraryName: 'd3', scriptPath: 'd3.js'}, + {moduleName: 'react', dependedBy: [], libraryName: 'react', scriptPath: 'react.js', external: 'React'}, + {moduleName: 'react-dom', dependedBy: [], libraryName: 'react', scriptPath: 'react-dom.js', external: 'ReactDOM'}, + {moduleName: 'react-router', dependedBy: [], libraryName: 'react-router', scriptPath: 'ReactRouter.js', external: 'ReactRouter'}, + {moduleName: 'moment', dependedBy: ['moment-timezone'], libraryName: 'moment.js', scriptPath: 'moment.js'}, + {moduleName: 'moment-timezone', dependedBy: [], libraryName: 'moment-timezone', scriptPath: 'moment-timezone-with-data.js', external: 'moment'} +] +/* eslint-enable max-len */ + +const cssServedFromCDN = modulesServedFromCDN.reduce(function (prev, curr) { + if (curr.cssPath !== undefined) prev.push(buildCSSURL(curr)) + return prev +}, []) + +const scriptsServedFromCDN = modulesServedFromCDN.reduce(function (prev, curr) { + if (curr.scriptPath !== undefined) prev.push(buildScriptURL(curr)) + return prev +}, []) + export default function (config) { const paths = config.utils_paths const {__DEV__, __PROD__, __TEST__, __COVERAGE__} = config.globals @@ -40,8 +67,7 @@ export default function (config) { webpackConfig.entry = { app: __DEV__ ? APP_ENTRY_PATHS.concat(`webpack-hot-middleware/client?path=${config.compiler_public_path}__webpack_hmr`) - : APP_ENTRY_PATHS, - vendor: config.compiler_vendor + : APP_ENTRY_PATHS } // ------------------------------------ @@ -52,6 +78,12 @@ export default function (config) { path: paths.dist(), publicPath: config.compiler_public_path } + if (!__TEST__) { + webpackConfig.externals = modulesServedFromCDN.reduce(function (prev, curr) { + if (curr.external !== undefined) prev[curr.moduleName] = curr.external + return prev + }, {}) + } // ------------------------------------ // Plugins @@ -66,12 +98,13 @@ export default function (config) { inject: 'body', minify: { collapseWhitespace: true - } + }, + stylesheets: cssServedFromCDN, + scripts: scriptsServedFromCDN }), new CopyWebpackPlugin([ { from: 'config/settings.js' } - ]), - new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) + ]) ] if (__DEV__) { @@ -95,15 +128,6 @@ export default function (config) { ) } - // Don't split bundles during testing, since we only want import one bundle - if (!__TEST__) { - webpackConfig.plugins.push( - new webpack.optimize.CommonsChunkPlugin({ - names: ['vendor'] - }) - ) - } - // ------------------------------------ // Pre-Loaders // ------------------------------------ diff --git a/packages/frontend/config/index.js b/packages/frontend/config/index.js index a4dd8b99..5910a9fb 100644 --- a/packages/frontend/config/index.js +++ b/packages/frontend/config/index.js @@ -44,16 +44,6 @@ export default function () { chunkModules : false, colors : true }, - compiler_vendor : [ - 'history', - 'react', - 'react-dom', - 'react-redux', - 'react-router', - 'react-router-redux', - 'redux', - 'moment-timezone' - ], compiler_alias: {}, // ---------------------------------- @@ -88,22 +78,6 @@ export default function () { '__BASENAME__' : JSON.stringify(process.env.BASENAME || '') } - // ------------------------------------ - // Validate Vendor Dependencies - // ------------------------------------ - const pkg = require('../package.json') - - config.compiler_vendor = config.compiler_vendor - .filter((dep) => { - if (pkg.dependencies[dep]) return true - - debug( - `Package "${dep}" was not found as an npm dependency in package.json; ` + - `it won't be included in the webpack vendor bundle. - Consider removing it from vendor_dependencies in ~/config/index.js` - ) - }) - // ------------------------------------ // Utilities // ------------------------------------ diff --git a/packages/frontend/src/components/common/MetricsGraph/MetricsGraph.js b/packages/frontend/src/components/common/MetricsGraph/MetricsGraph.js index 6484809b..a64c8d21 100644 --- a/packages/frontend/src/components/common/MetricsGraph/MetricsGraph.js +++ b/packages/frontend/src/components/common/MetricsGraph/MetricsGraph.js @@ -1,7 +1,6 @@ import React, { PropTypes } from 'react' import classnames from 'classnames' import c3 from 'c3' -import 'c3/c3.css' import { timeframes, getXAxisFormat, getTooltipTitleFormat, getIncrementTimestampFunc, getNumDates } from 'utils/status' import classes from './MetricsGraph.scss' import './MetricsGraph.global.scss' diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html index 3aa4321c..51946653 100644 --- a/packages/frontend/src/index.html +++ b/packages/frontend/src/index.html @@ -7,9 +7,15 @@ + <% for (var i in htmlWebpackPlugin.options.stylesheets) { %> + + <% } %>
+ <% for (var i in htmlWebpackPlugin.options.scripts) { %> + + <% } %> diff --git a/packages/lambda/src/utils/cache.js b/packages/lambda/src/utils/cache.js index 5a022a61..18a742c7 100644 --- a/packages/lambda/src/utils/cache.js +++ b/packages/lambda/src/utils/cache.js @@ -1,5 +1,6 @@ const oneYearBySeconds = 31536000 const oneDayBySeconds = 86400 +const tenSeconds = 10 export const getCacheControl = (contentType) => { const prefix = 'max-age=' @@ -11,6 +12,7 @@ export const getCacheControl = (contentType) => { return prefix + oneDayBySeconds case 'text/html': case 'application/json': + return prefix + tenSeconds default: return prefix + 0 } diff --git a/packages/lambda/test/aws/s3.js b/packages/lambda/test/aws/s3.js index f9453167..c16ac362 100644 --- a/packages/lambda/test/aws/s3.js +++ b/packages/lambda/test/aws/s3.js @@ -46,7 +46,7 @@ describe('S3', () => { const objectName = 'test.html' const body = 'data' const contentType = 'text/html' - const cacheControl = 'max-age=0' + const cacheControl = 'max-age=10' let actual AWS.mock('S3', 'putObject', (params, callback) => { actual = params @@ -119,7 +119,7 @@ describe('S3', () => { const destBucketName = 'test' const destObjectName = 'test.html' const contentType = 'text/html' - const cacheControl = 'max-age=0' + const cacheControl = 'max-age=10' let actual AWS.mock('S3', 'copyObject', (params, callback) => { actual = params diff --git a/packages/lambda/test/utils/cache.js b/packages/lambda/test/utils/cache.js index ab12dbbd..d1552982 100644 --- a/packages/lambda/test/utils/cache.js +++ b/packages/lambda/test/utils/cache.js @@ -5,7 +5,7 @@ describe('cache', () => { describe('getCacheControl', () => { it('should return CacheControl value', async () => { assert(getCacheControl('application/javascript') === 'max-age=31536000') - assert(getCacheControl('text/html') === 'max-age=0') + assert(getCacheControl('text/html') === 'max-age=10') assert(getCacheControl() === 'max-age=0') }) })