diff --git a/.eslintrc b/.eslintrc index e601af2e889..0425f8ded49 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,6 +5,7 @@ "extends": "airbnb", "globals": { "CLIENT": true, + "CLIENT_CONFIG": true, "DEVELOPMENT": true, "SERVER": true, "assert": true, diff --git a/bin/server.js b/bin/server.js index 2313e9d9f63..5cf9af9c07b 100644 --- a/bin/server.js +++ b/bin/server.js @@ -1,10 +1,9 @@ #!/usr/bin/env node require('babel-register'); -const config = require('config').default; -const env = config.get('env'); +const config = require('config'); -if (env === 'development') { +if (config.util.getEnv('NODE_ENV') === 'development') { if (!require('piping')({ hook: true, ignore: /(\/\.|~$|\.json|\.scss$)/i, diff --git a/bin/webpack-dev-server.js b/bin/webpack-dev-server.js index 9581d462d5e..a38e8abf6fa 100644 --- a/bin/webpack-dev-server.js +++ b/bin/webpack-dev-server.js @@ -8,9 +8,9 @@ const Express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackHotMiddleware = require('webpack-hot-middleware'); -const webpackDevConfig = require('config/webpack.dev.config.babel').default; +const webpackDevConfig = require('webpack.dev.config.babel').default; -const config = require('config').default; +const config = require('config'); const host = config.get('webpackServerHost'); const port = config.get('webpackServerPort'); diff --git a/config/default-disco.js b/config/default-disco.js new file mode 100644 index 00000000000..f053ebf7976 --- /dev/null +++ b/config/default-disco.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/config/default-search.js b/config/default-search.js new file mode 100644 index 00000000000..f053ebf7976 --- /dev/null +++ b/config/default-search.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/config/default.js b/config/default.js new file mode 100644 index 00000000000..5a0efdf462c --- /dev/null +++ b/config/default.js @@ -0,0 +1,79 @@ +// CONFIG defaults (aka PRODUCTION) +// WARNING: No test/stage/dev/development config should +// live here. + +/* eslint-disable object-shorthand */ + +const path = require('path'); +const defer = require('config/defer').deferConfig; + +const appName = process.env.NODE_APP_INSTANCE || null; +const validAppNames = [ + 'disco', + 'search', +]; + +// Throw if the appName supplied is not valid. +if (appName && validAppNames.indexOf(appName) === -1) { + throw new Error(`App ${appName} is not enabled`); +} + +module.exports = { + appName: appName, + basePath: path.resolve(__dirname, '../'), + + // 2592000 is 30 days in seconds. + cookieMaxAge: 2592000, + cookieName: 'jwt_api_auth_token', + + // The canonical list of enabled apps. + validAppNames: validAppNames, + + // The node server host and port. + serverHost: '127.0.0.1', + serverPort: 4000, + + // The CDN host for AMO. + amoCDN: 'https://addons.cdn.mozilla.net', + + apiHost: 'https://addons.mozilla.org', + apiPath: '/api/v3', + apiBase: defer((cfg) => cfg.apiHost + cfg.apiPath), + startLoginUrl: defer((cfg) => `${cfg.apiBase}/internal/accounts/login/start/`), + + // The keys listed here will be exposed on the client. + // Since by definition client-side code is public these config keys + // must not contain sensitive data. + clientConfigKeys: [ + 'apiBase', + 'cookieName', + 'cookieMaxAge', + 'startLoginUrl', + ], + + // Content security policy. + CSP: { + directives: { + defaultSrc: ["'self'"], + connectSrc: defer((cfg) => ["'self'", cfg.apiHost]), + imgSrc: defer((cfg) => [ + "'self'", + cfg.amoCDN, + ]), + scriptSrc: ["'self'"], + styleSrc: ["'self'"], + reportUri: '/__cspreport__', + }, + + // Set to true if you only want browsers to report errors, not block them + reportOnly: false, + + // Set to true if you want to blindly set all headers: Content-Security-Policy, + // X-WebKit-CSP, and X-Content-Security-Policy. + setAllHeaders: false, + + // Set to true if you want to disable CSP on Android where it can be buggy. + disableAndroid: false, + }, + +}; diff --git a/config/dev.js b/config/dev.js new file mode 100644 index 00000000000..cb733dd221f --- /dev/null +++ b/config/dev.js @@ -0,0 +1,6 @@ +// Config for the -dev server. + +module.exports = { + apiHost: 'https://addons-dev.allizom.org', + amoCDN: 'https://addons-dev-cdn.allizom.org', +}; diff --git a/config/development.js b/config/development.js new file mode 100644 index 00000000000..cf5d013e209 --- /dev/null +++ b/config/development.js @@ -0,0 +1,23 @@ +// Config specific to local development + +const defer = require('config/defer').deferConfig; + +module.exports = { + serverPort: 3000, + + apiHost: 'https://addons-dev.allizom.org', + amoCDN: 'https://addons-dev-cdn.allizom.org', + + webpackServerHost: '127.0.0.1', + webpackServerPort: 3001, + webpackHost: defer((cfg) => `http://${cfg.webpackServerHost}:${cfg.webpackServerPort}`), + + CSP: { + directives: { + connectSrc: defer((cfg) => ["'self'", cfg.apiHost, cfg.webpackHost]), + scriptSrc: defer((cfg) => ["'self'", cfg.webpackHost]), + styleSrc: ["'self'", 'blob:'], + }, + reportOnly: true, + }, +}; diff --git a/config/production.js b/config/production.js new file mode 100644 index 00000000000..8acf8cba0a7 --- /dev/null +++ b/config/production.js @@ -0,0 +1,2 @@ +// The default conf should cover this. +module.exports = {}; diff --git a/config/stage.js b/config/stage.js new file mode 100644 index 00000000000..3b7fbfc9ac6 --- /dev/null +++ b/config/stage.js @@ -0,0 +1,6 @@ +// Config for the stage server. + +module.exports = { + apiHost: 'https://addons.allizom.org', + amoCDN: 'https://addons-stage-cdn.allizom.org', +}; diff --git a/karma.conf.js b/karma.conf.js index 2948148adeb..26261deec74 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,35 +1,50 @@ // Karma configuration -/* eslint-disable no-console */ +/* eslint-disable max-len, no-console */ require('babel-register'); +const fs = require('fs'); + +const babelrc = fs.readFileSync('./.babelrc'); +const babelQuery = JSON.parse(babelrc); const webpack = require('webpack'); -const webpackDevConfig = require('./src/config/webpack.dev.config.babel').default; +const webpackConfigProd = require('./webpack.prod.config.babel').default; +const config = require('config'); +const getClientConfig = require('src/core/utils').getClientConfig; +const clientConfig = getClientConfig(config); const coverageReporters = [{ type: 'text-summary', }]; -const newWebpackConfig = Object.assign({}, webpackDevConfig, { + +const newWebpackConfig = Object.assign({}, webpackConfigProd, { plugins: [ new webpack.DefinePlugin({ - DEVELOPMENT: true, + DEVELOPMENT: false, CLIENT: true, SERVER: false, + CLIENT_CONFIG: JSON.stringify(clientConfig), }), - new webpack.EnvironmentPlugin([ - 'NODE_ENV', - 'API_HOST', - ]), + new webpack.NormalModuleReplacementPlugin(/config$/, 'client-config.js'), ], devtool: 'inline-source-map', - module: Object.assign({}, webpackDevConfig.module, { + module: { preLoaders: [{ test: /\.jsx?$/, loader: 'babel-istanbul', include: /src\//, exclude: /node_modules/, }], - }), + loaders: [{ + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel', + query: babelQuery, + }, { + test: /\.scss$/, + loader: 'style!css?importLoaders=2!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded', + }], + }, output: undefined, entry: undefined, }); @@ -48,8 +63,8 @@ if (process.env.TRAVIS) { coverageReporters.push({type: 'html', dir: 'coverage', subdir: '.'}); } -module.exports = function karmaConf(config) { - config.set({ +module.exports = function karmaConf(conf) { + conf.set({ coverageReporter: { reporters: coverageReporters, }, diff --git a/package.json b/package.json index f4f0bbd17a2..904a637b900 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "better-npm-run build", "build:disco": "better-npm-run build:disco", "build:search": "better-npm-run build:search", - "clean": "rimraf './dist/*+(css|js|map)' './src/webpack-assets.json' './src/sri.json'", + "clean": "rimraf './dist/*+(css|js|map|json)' './webpack-assets.json'", "dev:disco": "better-npm-run dev:disco", "dev:search": "better-npm-run dev:search", "eslint": "npm run lint", @@ -16,83 +16,102 @@ "servertest": "npm run build && better-npm-run servertest", "start:disco": "better-npm-run start:disco", "start:search": "better-npm-run start:search", - "test": "npm run version-check && npm run unittest && npm run servertest && npm run lint", - "unittest": "karma start --single-run", - "unittest:dev": "karma start", + "test": "better-npm-run test", + "unittest": "better-npm-run unittest", + "unittest:dev": "better-npm-run unittest:dev", "version-check": "node bin/version-check.js", "webpack-dev-server": "better-npm-run webpack-dev-server" }, "betterScripts": { "build": { - "command": "npm run clean && npm run version-check && webpack --verbose --display-error-details --progress --colors --config src/config/webpack.prod.config.babel.js", + "command": "npm run clean && npm run version-check && webpack --verbose --display-error-details --progress --colors --config webpack.prod.config.babel.js", "env": { - "NODE_ENV": "production", - "NODE_PATH": "$NODE_PATH:./src" + "NODE_PATH": "./:./src" } }, "build:disco": { "command": "npm run build", "env": { - "APP_NAME": "disco" + "NODE_APP_INSTANCE": "disco" } }, "build:search": { "command": "npm run build", "env": { - "APP_NAME": "search" + "NODE_APP_INSTANCE": "search" } }, "dev:disco": { "command": "better-npm-run start-dev", "env": { - "APP_NAME": "disco" + "NODE_APP_INSTANCE": "disco" } }, "dev:search": { "command": "better-npm-run start-dev", "env": { - "APP_NAME": "search" + "NODE_APP_INSTANCE": "search" } }, "start-dev": { "command": "npm run clean && concurrently --kill-others 'npm run webpack-dev-server' 'node bin/server.js'", "env": { + "ENABLE_PIPING": "true", "NODE_ENV": "development", - "NODE_PATH": "$NODE_PATH:./src", - "ENABLE_PIPING": "true" + "NODE_PATH": "./:./src" } }, "servertest": { "command": "mocha --compilers js:babel-register tests/server/", "env": { - "NODE_ENV": "production", - "NODE_PATH": "$NODE_PATH:./src:tests/server" + "NODE_PATH": "./:./src", + "NODE_ENV": "production" } }, "start": { "command": "npm run version-check && node bin/server.js", "env": { - "NODE_ENV": "production", - "NODE_PATH": "$NODE_PATH:./src" + "NODE_PATH": "./:./src" } }, "start:search": { "command": "better-npm-run start", "env": { - "APP_NAME": "search" + "NODE_APP_INSTANCE": "search" } }, "start:disco": { "command": "better-npm-run start", "env": { - "APP_NAME": "disco" + "NODE_APP_INSTANCE": "disco" + } + }, + "test": { + "command": "npm run version-check && npm run unittest && npm run servertest && npm run lint", + "env": { + "NODE_PATH": "./:./src", + "NODE_ENV": "production" + } + }, + "unittest": { + "command": "karma start --single-run", + "env": { + "NODE_PATH": "./:./src", + "NODE_ENV": "production" + } + }, + "unittest:dev": { + "command": "karma start", + "env": { + "NODE_PATH": "./:./src", + "NODE_ENV": "production" } }, "webpack-dev-server": { "command": "node bin/webpack-dev-server.js", "env": { "NODE_ENV": "development", - "NODE_PATH": "$NODE_PATH:./src" + "NODE_PATH": "./:./src" } } }, @@ -110,6 +129,7 @@ "better-npm-run": "0.0.8", "camelcase": "2.1.1", "common-tags": "0.0.3", + "config": "1.20.1", "express": "4.13.4", "extract-text-webpack-plugin": "1.0.1", "helmet": "1.3.0", @@ -170,6 +190,7 @@ "react-hot-loader": "1.3.0", "react-transform-hmr": "1.0.4", "redux-devtools": "3.2.0", + "require-uncached": "1.0.2", "rimraf": "2.5.2", "sass-loader": "3.1.2", "semver": "5.1.0", diff --git a/src/client-config.js b/src/client-config.js new file mode 100644 index 00000000000..80be370a6b9 --- /dev/null +++ b/src/client-config.js @@ -0,0 +1,14 @@ +/* + * This module is a stand-in for the config module + * when imported on the client. + * When webpack builds the client-side code it exposes + * the clientConfig config via the definePlugin as CLIENT_CONFIG. + */ + +const clientConfig = new Map(); + +Object.keys(CLIENT_CONFIG).forEach((key) => { + clientConfig.set(key, CLIENT_CONFIG[key]); +}); + +module.exports = clientConfig; diff --git a/src/config/index.js b/src/config/index.js deleted file mode 100644 index 040a44b7967..00000000000 --- a/src/config/index.js +++ /dev/null @@ -1,92 +0,0 @@ -import path from 'path'; -const config = new Map(); - -/* istanbul ignore next */ -// Looks like you can't ignore a file but you can ignore a function, we don't want coverage here. -(function defineConfig() { - const NODE_ENV = process.env.NODE_ENV; - - // Default to production unless overridden. - config.set('env', NODE_ENV || 'production'); - - config.set('basePath', path.resolve(__dirname, '../')); - - // This is the host / port for running as production. - config.set('serverHost', process.env.SERVER_HOST || '127.0.0.1'); - config.set('serverPort', process.env.SERVER_PORT || 4000); - - // This is the host / port for the development instance. - config.set('devServerHost', process.env.SERVER_HOST || '127.0.0.1'); - config.set('devServerPort', process.env.SERVER_PORT || 3000); - - // This is the host / port for the webpack dev server. - config.set('webpackServerHost', process.env.WEBPACK_SERVER_HOST || '127.0.0.1'); - config.set('webpackServerPort', process.env.WEBPACK_SERVER_PORT || 3001); - - config.set('apiHost', - process.env.API_HOST || - (NODE_ENV === 'development' ? - 'https://addons-dev.allizom.org' : 'https://addons.mozilla.org')); - - config.set('apiPath', process.env.API_PATH || '/api/v3'); - config.set('apiBase', config.get('apiHost') + config.get('apiPath')); - - config.set('startLoginUrl', `${config.get('apiHost')}/api/v3/internal/accounts/login/start/`); - - // 2592000 is 30 days in seconds. - config.set('cookieMaxAge', 2592000); - config.set('cookieName', 'jwt_api_auth_token'); - - const CSP = { - directives: { - connectSrc: ["'self'", config.get('apiHost')], - defaultSrc: ["'self'"], - imgSrc: [ - "'self'", - // FIXME: This should be added via a separate config for -dev once #272 is fixed. - 'https://addons-dev-cdn.allizom.org/', - ], - scriptSrc: ["'self'"], - styleSrc: ["'self'"], - reportUri: '/__cspreport__', - }, - - // Set to true if you only want browsers to report errors, not block them - reportOnly: false, - - // Set to true if you want to blindly set all headers: Content-Security-Policy, - // X-WebKit-CSP, and X-Content-Security-Policy. - setAllHeaders: false, - - // Set to true if you want to disable CSP on Android where it can be buggy. - disableAndroid: false, - }; - - const WEBPACK_HOST = - `http://${config.get('webpackServerHost')}:${config.get('webpackServerPort')}`; - - if (config.get('env') === 'development') { - CSP.directives.scriptSrc.push(WEBPACK_HOST); - CSP.directives.styleSrc.push('blob:'); - CSP.directives.connectSrc.push(WEBPACK_HOST); - CSP.reportOnly = true; - } - - config.set('CSP', CSP); - - // This is the list of apps allowed to run. - const validAppNames = ['search', 'disco']; - config.set('validAppNames', validAppNames); - - // Create a list of apps to build targets for. - const appName = process.env.APP_NAME; - if (validAppNames.indexOf(appName) > -1) { - config.set('appsBuildList', [appName]); - config.set('currentApp', appName); - } else { - config.set('appsBuildList', validAppNames); - config.set('currentApp', null); - } -}()); - -export default config; diff --git a/src/core/client/index.js b/src/core/client/index.js deleted file mode 100644 index ae9489cd81d..00000000000 --- a/src/core/client/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import makeClient from './base'; -import routes from '../routes'; -import createStore from 'search/store'; - -makeClient(routes, createStore); diff --git a/src/core/server/base.js b/src/core/server/base.js index ddfe142fabf..64f74e59ffe 100644 --- a/src/core/server/base.js +++ b/src/core/server/base.js @@ -13,19 +13,21 @@ import { match } from 'react-router'; import { ReduxAsyncConnect, loadOnServer } from 'redux-async-connect'; import WebpackIsomorphicTools from 'webpack-isomorphic-tools'; -import WebpackIsomorphicToolsConfig from 'config/webpack-isomorphic-tools'; +import WebpackIsomorphicToolsConfig from 'webpack-isomorphic-tools-config'; import config from 'config'; import { setJWT } from 'core/actions'; +const env = config.util.getEnv('NODE_ENV'); +const isDeployed = ['stage', 'dev', 'production'].indexOf(env) > -1; -const ENV = config.get('env'); -const APP_NAME = config.get('currentApp'); +const errorString = 'Internal Server Error'; +const appName = config.get('appName'); // Globals (these are set by definePlugin for client-side builds). global.CLIENT = false; global.SERVER = true; -global.DEVELOPMENT = ENV !== 'production'; +global.DEVELOPMENT = env === 'development'; export default function(routes, createStore) { const app = new Express(); @@ -43,20 +45,20 @@ export default function(routes, createStore) { // CSP configuration. app.use(helmet.csp(config.get('CSP'))); - if (ENV === 'development') { + if (env === 'development') { console.log('Running in Development Mode'); // eslint-disable-line no-console // clear require() cache if in development mode webpackIsomorphicTools.refresh(); } - app.use(Express.static(path.join(__dirname, '../../../dist'))); + app.use(Express.static(path.join(config.get('basePath'), 'dist'))); - // Return 200 for csp reports - this will need to be overridden in production. + // Return 200 for csp reports - this will need to be overridden when deployed. app.post('/__cspreport__', (req, res) => res.status(200).end('ok')); // Redirect from / for the search app it's a 302 to prevent caching. - if (APP_NAME === 'search') { + if (appName === 'search') { app.get('/', (req, res) => res.redirect(302, '/search')); } @@ -66,7 +68,7 @@ export default function(routes, createStore) { if (err) { console.error(err); // eslint-disable-line no-console - return res.status(500).end('Internal server error'); + return res.status(500).end(errorString); } if (!renderProps) { @@ -91,14 +93,14 @@ export default function(routes, createStore) { const assets = webpackIsomorphicTools.assets(); - // Get SRI for production only. - const sri = (ENV === 'production') ? JSON.parse( - fs.readFileSync(path.join(config.get('basePath'), 'sri.json')) + // Get SRI for deployed services only. + const sri = (isDeployed) ? JSON.parse( + fs.readFileSync(path.join(config.get('basePath'), 'dist/sri.json')) ) : {}; const styles = Object.keys(assets.styles).map((style) => { const cssHash = sri[path.basename(assets.styles[style])]; - if (ENV === 'production' && !cssHash) { + if (isDeployed && !cssHash) { throw new Error('Missing SRI Data'); } const cssSRI = sri && cssHash ? ` integrity="${cssHash}" crossorigin="anonymous"` : ''; @@ -108,7 +110,7 @@ export default function(routes, createStore) { const script = Object.keys(assets.javascript).map((js) => { const jsHash = sri[path.basename(assets.javascript[js])]; - if (ENV === 'production' && !jsHash) { + if (isDeployed && !jsHash) { throw new Error('Missing SRI Data'); } const jsSRI = sri && jsHash ? ` integrity="${jsHash}" crossorigin="anonymous"` : ''; @@ -135,41 +137,51 @@ export default function(routes, createStore) { res.header('Content-Type', 'text/html'); return res.end(HTML); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(error.stack); + res.status(500).end(errorString); }); }); }); + // eslint-disable-next-line no-unused-vars + app.use((err, req, res, next) => { + // eslint-disable-next-line no-console + console.error(err.stack); + res.status(500).end(errorString); + }); + return app; } -export function runServer({listen = true, appName = APP_NAME} = {}) { - if (!appName) { +export function runServer({listen = true, app = appName} = {}) { + if (!app) { // eslint-disable-next-line no-console console.log(`Please specify a valid appName from ${config.get('validAppNames')}`); process.exit(1); } - const port = ENV === 'production' ? - config.get('serverPort') : config.get('devServerPort'); - const host = ENV === 'production' ? - config.get('serverHost') : config.get('devServerHost'); + const port = config.get('serverPort'); + const host = config.get('serverHost'); const isoMorphicServer = new WebpackIsomorphicTools(WebpackIsomorphicToolsConfig); - return isoMorphicServer.development(ENV === 'development') + return isoMorphicServer.development(env === 'development') .server(config.get('basePath')) .then(() => { global.webpackIsomorphicTools = isoMorphicServer; // Webpack Isomorphic tools is ready // now fire up the actual server. return new Promise((resolve, reject) => { - const server = require(`${appName}/server`).default; + const server = require(`${app}/server`).default; if (listen === true) { server.listen(port, host, (err) => { if (err) { reject(err); } // eslint-disable-next-line no-console - console.log(`🔥 Addons-frontend server is running [ENV:${ENV}] [APP:${appName}]`); + console.log(`🔥 Addons-frontend server is running [ENV:${env}] [APP:${app}]`); // eslint-disable-next-line no-console console.log(`👁 Open your browser at http://${host}:${port} to view it.`); resolve(server); diff --git a/src/core/utils.js b/src/core/utils.js index 72c9fa9d374..739dff6def6 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -18,3 +18,11 @@ export function camelCaseProps(obj) { }); return newObj; } + +export function getClientConfig(config) { + const clientConfig = {}; + for (const key of config.get('clientConfigKeys')) { + clientConfig[key] = config.get(key); + } + return clientConfig; +} diff --git a/src/disco/containers/DiscoPane.js b/src/disco/containers/DiscoPane.js index c740274ac26..ebeca80c178 100644 --- a/src/disco/containers/DiscoPane.js +++ b/src/disco/containers/DiscoPane.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { asyncConnect } from 'redux-async-connect'; import { camelCaseProps } from 'core/utils'; -import Addon from '../components/Addon'; +import Addon from 'disco/components/Addon'; import fakeData from 'disco/fakeData'; diff --git a/src/search/containers/CurrentSearchPage.js b/src/search/containers/CurrentSearchPage.js index add7f155675..f77511759f8 100644 --- a/src/search/containers/CurrentSearchPage.js +++ b/src/search/containers/CurrentSearchPage.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { asyncConnect } from 'redux-async-connect'; -import SearchPage from '../components/SearchPage'; -import { searchStart, searchLoad, searchFail } from '../actions'; +import SearchPage from 'search/components/SearchPage'; +import { searchStart, searchLoad, searchFail } from 'search/actions'; import { search } from 'core/api'; export function mapStateToProps(state) { diff --git a/test-runner.js b/test-runner.js index 2dfe29ac8e8..8734926bbb2 100644 --- a/test-runner.js +++ b/test-runner.js @@ -1,6 +1,6 @@ require('babel-polyfill'); -const testsContext = require.context('./tests/karma/', true, /\.js$/); +const testsContext = require.context('./tests/client/', true, /\.js$/); const componentsContext = require.context( // This regex excludes server.js or server/*.js './src/', true, /^(?:(?!server|config|client).)*\.js$/); diff --git a/tests/karma/core/actions/test_index.js b/tests/client/core/actions/test_index.js similarity index 100% rename from tests/karma/core/actions/test_index.js rename to tests/client/core/actions/test_index.js diff --git a/tests/karma/core/api/test_api.js b/tests/client/core/api/test_api.js similarity index 100% rename from tests/karma/core/api/test_api.js rename to tests/client/core/api/test_api.js diff --git a/public/.gitkeep b/tests/client/core/components/.gitkeep similarity index 100% rename from public/.gitkeep rename to tests/client/core/components/.gitkeep diff --git a/tests/karma/core/components/TestLoginPage.js b/tests/client/core/components/TestLoginPage.js similarity index 100% rename from tests/karma/core/components/TestLoginPage.js rename to tests/client/core/components/TestLoginPage.js diff --git a/tests/karma/core/components/TestNotFound.js b/tests/client/core/components/TestNotFound.js similarity index 100% rename from tests/karma/core/components/TestNotFound.js rename to tests/client/core/components/TestNotFound.js diff --git a/tests/karma/core/components/TestPaginate.js b/tests/client/core/components/TestPaginate.js similarity index 100% rename from tests/karma/core/components/TestPaginate.js rename to tests/client/core/components/TestPaginate.js diff --git a/tests/karma/core/containers/TestHandleLogin.js b/tests/client/core/containers/TestHandleLogin.js similarity index 100% rename from tests/karma/core/containers/TestHandleLogin.js rename to tests/client/core/containers/TestHandleLogin.js diff --git a/tests/karma/core/containers/TestLoginRequired.js b/tests/client/core/containers/TestLoginRequired.js similarity index 96% rename from tests/karma/core/containers/TestLoginRequired.js rename to tests/client/core/containers/TestLoginRequired.js index be5394adaec..37228e9980a 100644 --- a/tests/karma/core/containers/TestLoginRequired.js +++ b/tests/client/core/containers/TestLoginRequired.js @@ -1,6 +1,6 @@ import React from 'react'; -import { shallowRender } from '../../../utils'; +import { shallowRender } from 'tests/client/helpers'; import { mapStateToProps, LoginRequired } from 'core/containers/LoginRequired'; import LoginPage from 'core/components/LoginPage'; diff --git a/tests/karma/core/reducers/test_addons.js b/tests/client/core/reducers/test_addons.js similarity index 100% rename from tests/karma/core/reducers/test_addons.js rename to tests/client/core/reducers/test_addons.js diff --git a/tests/karma/core/reducers/test_api.js b/tests/client/core/reducers/test_api.js similarity index 100% rename from tests/karma/core/reducers/test_api.js rename to tests/client/core/reducers/test_api.js diff --git a/tests/karma/core/reducers/test_authentication.js b/tests/client/core/reducers/test_authentication.js similarity index 100% rename from tests/karma/core/reducers/test_authentication.js rename to tests/client/core/reducers/test_authentication.js diff --git a/tests/client/core/test_utils.js b/tests/client/core/test_utils.js new file mode 100644 index 00000000000..0a73ca69c90 --- /dev/null +++ b/tests/client/core/test_utils.js @@ -0,0 +1,39 @@ +import { camelCaseProps, getClientConfig } from 'core/utils'; + +describe('camelCaseProps', () => { + const input = { + underscore_delimited: 'underscore', + 'hyphen-delimited': 'hyphen', + 'period.delimited': 'period', + }; + + const result = camelCaseProps(input); + + it('deals with hyphenated props', () => { + assert.equal(result.hyphenDelimited, 'hyphen'); + }); + + it('deals with underscore delimited props', () => { + assert.equal(result.underscoreDelimited, 'underscore'); + }); + + it('deals with period delimited props', () => { + assert.equal(result.periodDelimited, 'period'); + }); +}); + + +describe('getClientConfig', () => { + const fakeConfig = new Map(); + fakeConfig.set('hai', 'there'); + fakeConfig.set('what', 'evar'); + fakeConfig.set('secret', 'sauce'); + fakeConfig.set('clientConfigKeys', ['hai', 'what']); + + it('should add config data to object', () => { + const clientConfig = getClientConfig(fakeConfig); + assert.equal(clientConfig.hai, 'there'); + assert.equal(clientConfig.what, 'evar'); + assert.equal(clientConfig.secret, undefined); + }); +}); diff --git a/tests/karma/disco/components/TestAddon.js b/tests/client/disco/components/TestAddon.js similarity index 100% rename from tests/karma/disco/components/TestAddon.js rename to tests/client/disco/components/TestAddon.js diff --git a/tests/karma/disco/components/TestInstallButton.js b/tests/client/disco/components/TestInstallButton.js similarity index 100% rename from tests/karma/disco/components/TestInstallButton.js rename to tests/client/disco/components/TestInstallButton.js diff --git a/tests/karma/core/components/.gitkeep b/tests/client/disco/containers/.gitkeep similarity index 100% rename from tests/karma/core/components/.gitkeep rename to tests/client/disco/containers/.gitkeep diff --git a/tests/karma/disco/containers/TestApp.js b/tests/client/disco/containers/TestApp.js similarity index 88% rename from tests/karma/disco/containers/TestApp.js rename to tests/client/disco/containers/TestApp.js index d132e92c5a8..a989a975a6d 100644 --- a/tests/karma/disco/containers/TestApp.js +++ b/tests/client/disco/containers/TestApp.js @@ -1,7 +1,7 @@ import React from 'react'; import App from 'disco/containers/App'; -import { shallowRender } from '../../../utils'; +import { shallowRender } from 'tests/client/helpers'; describe('App', () => { it('renders its children', () => { diff --git a/tests/karma/disco/containers/TestDiscoPane.js b/tests/client/disco/containers/TestDiscoPane.js similarity index 100% rename from tests/karma/disco/containers/TestDiscoPane.js rename to tests/client/disco/containers/TestDiscoPane.js diff --git a/tests/utils.js b/tests/client/helpers.js similarity index 100% rename from tests/utils.js rename to tests/client/helpers.js diff --git a/tests/karma/search/actions/test_index.js b/tests/client/search/actions/test_index.js similarity index 100% rename from tests/karma/search/actions/test_index.js rename to tests/client/search/actions/test_index.js diff --git a/tests/karma/disco/containers/.gitkeep b/tests/client/search/components/.gitkeep similarity index 100% rename from tests/karma/disco/containers/.gitkeep rename to tests/client/search/components/.gitkeep diff --git a/tests/karma/search/components/TestSearchForm.js b/tests/client/search/components/TestSearchForm.js similarity index 100% rename from tests/karma/search/components/TestSearchForm.js rename to tests/client/search/components/TestSearchForm.js diff --git a/tests/karma/search/components/TestSearchPage.js b/tests/client/search/components/TestSearchPage.js similarity index 96% rename from tests/karma/search/components/TestSearchPage.js rename to tests/client/search/components/TestSearchPage.js index 7974d3187e6..2ac996402ce 100644 --- a/tests/karma/search/components/TestSearchPage.js +++ b/tests/client/search/components/TestSearchPage.js @@ -4,7 +4,7 @@ import SearchPage from 'search/components/SearchPage'; import SearchResults from 'search/components/SearchResults'; import SearchForm from 'search/components/SearchForm'; import Paginate from 'core/components/Paginate'; -import { findAllByTag, findByTag, shallowRender } from '../../../utils'; +import { findAllByTag, findByTag, shallowRender } from 'tests/client/helpers'; describe('', () => { let props; diff --git a/tests/karma/search/components/TestSearchResult.js b/tests/client/search/components/TestSearchResult.js similarity index 100% rename from tests/karma/search/components/TestSearchResult.js rename to tests/client/search/components/TestSearchResult.js diff --git a/tests/karma/search/components/TestSearchResults.js b/tests/client/search/components/TestSearchResults.js similarity index 100% rename from tests/karma/search/components/TestSearchResults.js rename to tests/client/search/components/TestSearchResults.js diff --git a/tests/karma/search/containers/TestAddonPage.js b/tests/client/search/containers/TestAddonPage.js similarity index 100% rename from tests/karma/search/containers/TestAddonPage.js rename to tests/client/search/containers/TestAddonPage.js diff --git a/tests/karma/search/containers/TestApp.js b/tests/client/search/containers/TestApp.js similarity index 88% rename from tests/karma/search/containers/TestApp.js rename to tests/client/search/containers/TestApp.js index 7a01503276d..3b73ae71d39 100644 --- a/tests/karma/search/containers/TestApp.js +++ b/tests/client/search/containers/TestApp.js @@ -1,7 +1,7 @@ import React from 'react'; import App from 'search/containers/App'; -import { shallowRender } from '../../../utils'; +import { shallowRender } from 'tests/client/helpers'; describe('App', () => { it('renders its children', () => { diff --git a/tests/karma/search/containers/TestCurrentSearchPage.js b/tests/client/search/containers/TestCurrentSearchPage.js similarity index 100% rename from tests/karma/search/containers/TestCurrentSearchPage.js rename to tests/client/search/containers/TestCurrentSearchPage.js diff --git a/tests/karma/search/reducers/test_search.js b/tests/client/search/reducers/test_search.js similarity index 100% rename from tests/karma/search/reducers/test_search.js rename to tests/client/search/reducers/test_search.js diff --git a/tests/karma/search/test_store.js b/tests/client/search/test_store.js similarity index 100% rename from tests/karma/search/test_store.js rename to tests/client/search/test_store.js diff --git a/tests/karma/core/test-utils.js b/tests/karma/core/test-utils.js deleted file mode 100644 index 9e4bb4772dd..00000000000 --- a/tests/karma/core/test-utils.js +++ /dev/null @@ -1,23 +0,0 @@ -import { camelCaseProps } from 'core/utils'; - -describe('camelCaseProps', () => { - const input = { - underscore_delimited: 'underscore', - 'hyphen-delimited': 'hyphen', - 'period.delimited': 'period', - }; - - const result = camelCaseProps(input); - - it('deals with hyphenated props', () => { - assert.equal(result.hyphenDelimited, 'hyphen'); - }); - - it('deals with underscore delimited props', () => { - assert.equal(result.underscoreDelimited, 'underscore'); - }); - - it('deals with period delimited props', () => { - assert.equal(result.periodDelimited, 'period'); - }); -}); diff --git a/tests/karma/search/components/.gitkeep b/tests/karma/search/components/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/server/TestConfig.js b/tests/server/TestConfig.js new file mode 100644 index 00000000000..74fb5a80b84 --- /dev/null +++ b/tests/server/TestConfig.js @@ -0,0 +1,46 @@ +import { getClientConfig } from 'core/utils'; +import { assert } from 'chai'; +import requireUncached from 'require-uncached'; + + +describe('Config', () => { + afterEach(() => { + process.env.NODE_ENV = 'production'; + }); + + it('should provide a production config by default', () => { + process.env.NODE_ENV = 'production'; + const config = requireUncached('config'); + const clientConfig = getClientConfig(config); + assert.equal(config.get('apiHost'), 'https://addons.mozilla.org'); + assert.include(clientConfig.startLoginUrl, 'https://addons.mozilla.org'); + assert.equal(config.util.getEnv('NODE_ENV'), 'production'); + }); + + it('should provide a dev config', () => { + process.env.NODE_ENV = 'dev'; + const config = requireUncached('config'); + const clientConfig = getClientConfig(config); + assert.equal(config.get('apiHost'), 'https://addons-dev.allizom.org'); + assert.include(clientConfig.startLoginUrl, 'https://addons-dev.allizom.org'); + assert.equal(config.util.getEnv('NODE_ENV'), 'dev'); + }); + + it('should provide a stage config', () => { + process.env.NODE_ENV = 'stage'; + const config = requireUncached('config'); + const clientConfig = getClientConfig(config); + assert.equal(config.get('apiHost'), 'https://addons.allizom.org'); + assert.include(clientConfig.startLoginUrl, 'https://addons.allizom.org'); + assert.equal(config.util.getEnv('NODE_ENV'), 'stage'); + }); + + it('should provide a development config', () => { + process.env.NODE_ENV = 'development'; + const config = requireUncached('config'); + const clientConfig = getClientConfig(config); + assert.equal(config.get('apiHost'), 'https://addons-dev.allizom.org'); + assert.include(clientConfig.startLoginUrl, 'https://addons-dev.allizom.org'); + assert.equal(config.util.getEnv('NODE_ENV'), 'development'); + }); +}); diff --git a/tests/server/TestDiscoViews.js b/tests/server/TestDiscoViews.js index f86f5087d24..e9f81e2eb08 100644 --- a/tests/server/TestDiscoViews.js +++ b/tests/server/TestDiscoViews.js @@ -12,7 +12,7 @@ import { checkSRI } from './helpers'; describe('GET requests', () => { let app; - before((done) => runServer({listen: false, appName: 'disco'}) + before((done) => runServer({listen: false, app: 'disco'}) .then((server) => { app = server; done(); diff --git a/tests/server/TestSearchViews.js b/tests/server/TestSearchViews.js index 68751c3ce47..5934200fdb1 100644 --- a/tests/server/TestSearchViews.js +++ b/tests/server/TestSearchViews.js @@ -11,7 +11,7 @@ import { checkSRI } from './helpers'; describe('GET requests', () => { let app; - before((done) => runServer({listen: false, appName: 'search'}) + before((done) => runServer({listen: false, app: 'search'}) .then((server) => { app = server; done(); diff --git a/src/config/webpack-isomorphic-tools.js b/webpack-isomorphic-tools-config.js similarity index 100% rename from src/config/webpack-isomorphic-tools.js rename to webpack-isomorphic-tools-config.js diff --git a/src/config/webpack.dev.config.babel.js b/webpack.dev.config.babel.js similarity index 75% rename from src/config/webpack.dev.config.babel.js rename to webpack.dev.config.babel.js index 5df122bd22b..65027cfc8aa 100644 --- a/src/config/webpack.dev.config.babel.js +++ b/webpack.dev.config.babel.js @@ -4,13 +4,15 @@ import fs from 'fs'; import path from 'path'; import webpack from 'webpack'; -import webpackConfig from './webpack.prod.config.babel'; -import config from './index'; +import { getClientConfig } from 'core/utils'; +import config from 'config'; +import webpackConfig from './webpack.prod.config.babel'; import WebpackIsomorphicToolsPlugin from 'webpack-isomorphic-tools/plugin'; -import webpackIsomorphicToolsConfig from './webpack-isomorphic-tools'; +import webpackIsomorphicToolsConfig from './webpack-isomorphic-tools-config'; -const development = config.get('env') === 'development'; +const clientConfig = getClientConfig(config); +const localDevelopment = config.util.getEnv('NODE_ENV') === 'development'; const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(webpackIsomorphicToolsConfig); @@ -28,29 +30,30 @@ const babelDevPlugins = [['react-transform', { }]]; const BABEL_QUERY = Object.assign({}, babelrcObject, { - plugins: development ? babelPlugins.concat(babelDevPlugins) : babelPlugins, + plugins: localDevelopment ? babelPlugins.concat(babelDevPlugins) : babelPlugins, }); const webpackHost = config.get('webpackServerHost'); const webpackPort = config.get('webpackServerPort'); -const assetsPath = path.resolve(__dirname, '../../dist'); +const assetsPath = path.resolve(__dirname, 'dist'); const hmr = `webpack-hot-middleware/client?path=http://${webpackHost}:${webpackPort}/__webpack_hmr`; -const appsBuildList = config.get('appsBuildList'); +const appName = config.get('appName'); +const appsBuildList = appName ? [appName] : config.get('validAppNames'); const entryPoints = {}; for (const app of appsBuildList) { entryPoints[app] = [ hmr, - `${app}/client`, + `src/${app}/client`, ]; } export default Object.assign({}, webpackConfig, { devtool: 'inline-source-map', - context: path.resolve(__dirname, '..'), + context: path.resolve(__dirname), entry: entryPoints, output: Object.assign({}, webpackConfig.output, { path: assetsPath, @@ -71,14 +74,12 @@ export default Object.assign({}, webpackConfig, { }, plugins: [ new webpack.DefinePlugin({ - DEVELOPMENT: true, CLIENT: true, + CLIENT_CONFIG: JSON.stringify(clientConfig), + DEVELOPMENT: true, SERVER: false, }), - new webpack.EnvironmentPlugin([ - 'NODE_ENV', - 'API_HOST', - ]), + new webpack.NormalModuleReplacementPlugin(/config$/, 'client-config.js'), new webpack.HotModuleReplacementPlugin(), new webpack.IgnorePlugin(/webpack-stats\.json$/), webpackIsomorphicToolsPlugin.development(), diff --git a/src/config/webpack.prod.config.babel.js b/webpack.prod.config.babel.js similarity index 73% rename from src/config/webpack.prod.config.babel.js rename to webpack.prod.config.babel.js index dba37f8e91a..13249547d64 100644 --- a/src/config/webpack.prod.config.babel.js +++ b/webpack.prod.config.babel.js @@ -2,30 +2,34 @@ import path from 'path'; -import webpack from 'webpack'; +import config from 'config'; import ExtractTextPlugin from 'extract-text-webpack-plugin'; import WebpackIsomorphicToolsPlugin from 'webpack-isomorphic-tools/plugin'; -import webpackIsomorphicToolsConfig from './webpack-isomorphic-tools'; +import webpackIsomorphicToolsConfig from './webpack-isomorphic-tools-config'; +import webpack from 'webpack'; import SriStatsPlugin from 'sri-stats-webpack-plugin'; -import config from './index'; +import { getClientConfig } from 'core/utils'; + +const clientConfig = getClientConfig(config); -const appsBuildList = config.get('appsBuildList'); +const appName = config.get('appName'); +const appsBuildList = appName ? [appName] : config.get('validAppNames'); const entryPoints = {}; for (const app of appsBuildList) { - entryPoints[app] = `${app}/client`; + entryPoints[app] = `src/${app}/client`; } export default { devtool: 'source-map', - context: path.resolve(__dirname, '..'), + context: path.resolve(__dirname), progress: true, entry: entryPoints, output: { - path: path.join(__dirname, '../../dist'), + path: path.join(__dirname, 'dist'), filename: '[name]-[chunkhash].js', chunkFilename: '[name]-[chunkhash].js', publicPath: '/', @@ -48,16 +52,15 @@ export default { DEVELOPMENT: false, CLIENT: true, SERVER: false, + CLIENT_CONFIG: JSON.stringify(clientConfig), }), - new webpack.EnvironmentPlugin([ - 'NODE_ENV', - 'API_HOST', - ]), + // Replaces server config module with the subset clientConfig object. + new webpack.NormalModuleReplacementPlugin(/config$/, 'client-config.js'), new ExtractTextPlugin('[name]-[chunkhash].css', {allChunks: true}), new SriStatsPlugin({ algorithm: 'sha512', write: true, - saveAs: path.join(__dirname, '../sri.json'), + saveAs: path.join(__dirname, 'dist/sri.json'), }), // ignore dev config new webpack.IgnorePlugin(/\.\/webpack\.dev/, /\/babel$/), @@ -76,7 +79,7 @@ export default { 'normalize.css': 'normalize.css/normalize.css', }, root: [ - path.resolve('../src'), + path.resolve(__dirname), ], modulesDirectories: ['node_modules', 'src'], extensions: ['', '.js', '.jsx'],