Skip to content

Commit

Permalink
allow manual initialization and config as an arg
Browse files Browse the repository at this point in the history
  • Loading branch information
erquhart committed Mar 1, 2018
1 parent 7caa156 commit b61f41c
Show file tree
Hide file tree
Showing 15 changed files with 718 additions and 420 deletions.
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"setupFiles": [
"./setupTests.js"
],
"mapCoverage": true,
"coverageReporters": [
"lcov"
],
Expand Down Expand Up @@ -75,7 +74,7 @@
"babel": "^6.5.2",
"babel-cli": "^6.18.0",
"babel-core": "^6.23.1",
"babel-jest": "^21.2.0",
"babel-jest": "^22.0.0",
"babel-loader": "^7.0.0",
"babel-plugin-lodash": "^3.2.0",
"babel-plugin-module-resolver": "^3.0.0",
Expand All @@ -102,8 +101,8 @@
"file-loader": "^1.1.4",
"identity-obj-proxy": "^3.0.0",
"imports-loader": "^0.7.1",
"jest": "^21.2.1",
"jest-cli": "^21.2.1",
"jest": "^22.0.0",
"jest-cli": "^22.0.0",
"lint-staged": "^3.3.1",
"npm-check": "^5.2.3",
"postcss-cssnext": "^3.0.2",
Expand Down
55 changes: 29 additions & 26 deletions src/actions/__tests__/config.spec.js
Original file line number Diff line number Diff line change
@@ -1,117 +1,120 @@
import { fromJS } from 'immutable';
import { applyDefaults, validateConfig } from '../config';

describe('config', () => {
describe('applyDefaults', () => {
it('should set publish_mode if not set', () => {
expect(applyDefaults({
const config = fromJS({
foo: 'bar',
media_folder: 'path/to/media',
})).toEqual({
foo: 'bar',
publish_mode: 'simple',
media_folder: 'path/to/media',
public_folder: '/path/to/media',
});
expect(
applyDefaults(config)
).toEqual(
config.set('publish_mode', 'simple')
);
});

it('should set publish_mode from config', () => {
expect(applyDefaults({
foo: 'bar',
publish_mode: 'complex',
media_folder: 'path/to/media',
})).toEqual({
const config = fromJS({
foo: 'bar',
publish_mode: 'complex',
media_folder: 'path/to/media',
public_folder: '/path/to/media',
});
expect(
applyDefaults(config)
).toEqual(
config
);
});

it('should set public_folder based on media_folder if not set', () => {
expect(applyDefaults({
expect(applyDefaults(fromJS({
foo: 'bar',
media_folder: 'path/to/media',
})).toEqual({
}))).toEqual(fromJS({
foo: 'bar',
publish_mode: 'simple',
media_folder: 'path/to/media',
public_folder: '/path/to/media',
});
}));
});

it('should not overwrite public_folder if set', () => {
expect(applyDefaults({
expect(applyDefaults(fromJS({
foo: 'bar',
media_folder: 'path/to/media',
public_folder: '/publib/path',
})).toEqual({
}))).toEqual(fromJS({
foo: 'bar',
publish_mode: 'simple',
media_folder: 'path/to/media',
public_folder: '/publib/path',
});
}));
});
});

describe('validateConfig', () => {
it('should return the config if no errors', () => {
const config = { foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [{}] };
const config = fromJS({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [{}] });
expect(
validateConfig(config)
).toEqual(config);
});

it('should throw if backend is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar' });
validateConfig(fromJS({ foo: 'bar' }));
}).toThrowError('Error in configuration file: A `backend` wasn\'t found. Check your config.yml file.');
});

it('should throw if backend name is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: {} });
validateConfig(fromJS({ foo: 'bar', backend: {} }));
}).toThrowError('Error in configuration file: A `backend.name` wasn\'t found. Check your config.yml file.');
});

it('should throw if backend name is not a string in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: { } } });
validateConfig(fromJS({ foo: 'bar', backend: { name: { } } }));
}).toThrowError('Error in configuration file: Your `backend.name` must be a string. Check your config.yml file.');
});

it('should throw if media_folder is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' } });
validateConfig(fromJS({ foo: 'bar', backend: { name: 'bar' } }));
}).toThrowError('Error in configuration file: A `media_folder` wasn\'t found. Check your config.yml file.');
});

it('should throw if media_folder is not a string in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: {} });
validateConfig(fromJS({ foo: 'bar', backend: { name: 'bar' }, media_folder: {} }));
}).toThrowError('Error in configuration file: Your `media_folder` must be a string. Check your config.yml file.');
});

it('should throw if collections is not defined in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz' });
validateConfig(fromJS({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz' }));
}).toThrowError('Error in configuration file: A `collections` wasn\'t found. Check your config.yml file.');
});

it('should throw if collections not an array in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: {} });
validateConfig(fromJS({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: {} }));
}).toThrowError('Error in configuration file: Your `collections` must be an array with at least one element. Check your config.yml file.');
});

it('should throw if collections is an empty array in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [] });
validateConfig(fromJS({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [] }));
}).toThrowError('Error in configuration file: Your `collections` must be an array with at least one element. Check your config.yml file.');
});

it('should throw if collections is an array with a single null element in config', () => {
expect(() => {
validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [null] });
validateConfig(fromJS({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [null] }));
}).toThrowError('Error in configuration file: Your `collections` must be an array with at least one element. Check your config.yml file.');
});
});
Expand Down
80 changes: 52 additions & 28 deletions src/actions/config.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,63 @@
import yaml from "js-yaml";
import { set, defaultsDeep, get } from "lodash";
import { Map, List, fromJS } from "immutable";
import { trimStart, flow } from "lodash";
import { authenticateUser } from "Actions/auth";
import * as publishModes from "Constants/publishModes";

export const CONFIG_REQUEST = "CONFIG_REQUEST";
export const CONFIG_SUCCESS = "CONFIG_SUCCESS";
export const CONFIG_FAILURE = "CONFIG_FAILURE";
export const CONFIG_MERGE = "CONFIG_MERGE";

const defaults = {
publish_mode: publishModes.SIMPLE,
};

export function applyDefaults(config) {
// Make sure there is a public folder
set(defaults,
"public_folder",
config.media_folder.charAt(0) === "/" ? config.media_folder : `/${ config.media_folder }`);

return defaultsDeep(config, defaults);
return Map(defaults)
.mergeDeep(config)
.withMutations(map => {
/**
* Use media_folder as default public_folder.
*/
const defaultPublicFolder = `/${trimStart(map.get('media_folder'), '/')}`;
if (!map.get('public_folder')) {
map.set('public_folder', defaultPublicFolder);
}
});
}

export function validateConfig(config) {
if (!get(config, 'backend')) {
if (!config.get('backend')) {
throw new Error("Error in configuration file: A `backend` wasn't found. Check your config.yml file.");
}
if (!get(config, ['backend', 'name'])) {
if (!config.getIn(['backend', 'name'])) {
throw new Error("Error in configuration file: A `backend.name` wasn't found. Check your config.yml file.");
}
if (typeof config.backend.name !== 'string') {
if (typeof config.getIn(['backend', 'name']) !== 'string') {
throw new Error("Error in configuration file: Your `backend.name` must be a string. Check your config.yml file.");
}
if (!get(config, 'media_folder')) {
if (!config.get('media_folder')) {
throw new Error("Error in configuration file: A `media_folder` wasn\'t found. Check your config.yml file.");
}
if (typeof config.media_folder !== 'string') {
if (typeof config.get('media_folder') !== 'string') {
throw new Error("Error in configuration file: Your `media_folder` must be a string. Check your config.yml file.");
}
if (!get(config, 'collections')) {
if (!config.get('collections')) {
throw new Error("Error in configuration file: A `collections` wasn\'t found. Check your config.yml file.");
}
if (!Array.isArray(config.collections) || config.collections.length === 0 || !config.collections[0]) {
const collections = config.get('collections');
if (!List.isList(collections) || collections.isEmpty() || !collections.first()) {
throw new Error("Error in configuration file: Your `collections` must be an array with at least one element. Check your config.yml file.");
}
return config;
}

function mergePreloadedConfig(preloadedConfig, loadedConfig) {
const map = fromJS(loadedConfig) || Map();
return preloadedConfig ? preloadedConfig.mergeDeep(map) : map;
}

function parseConfig(data) {
const config = yaml.safeLoad(data);
if (typeof CMS_ENV === "string" && config[CMS_ENV]) {
Expand Down Expand Up @@ -82,29 +95,40 @@ export function configDidLoad(config) {
};
}

export function mergeConfig(config) {
return { type: CONFIG_MERGE, payload: config };
}

export function loadConfig() {
if (window.CMS_CONFIG) {
return configDidLoad(window.CMS_CONFIG);
return configDidLoad(fromJS(window.CMS_CONFIG));
}
return (dispatch) => {
return async (dispatch, getState) => {
dispatch(configLoading());

fetch("config.yml", { credentials: 'same-origin' })
.then((response) => {
if (response.status !== 200) {
try {
const preloadedConfig = getState().config;
const response = await fetch('config.yml', { credentials: 'same-origin' })
const requestSuccess = response.status === 200;

if (!preloadedConfig && !requestSuccess) {
throw new Error(`Failed to load config.yml (${ response.status })`);
}
return response.text();
})
.then(parseConfig)
.then(validateConfig)
.then(applyDefaults)
.then((config) => {

const loadedConfig = parseConfig(requestSuccess ? await response.text() : '');

/**
* Merge any existing configuration so the result can be validated.
*/
const mergedConfig = mergePreloadedConfig(preloadedConfig, loadedConfig)
const config = flow(validateConfig, applyDefaults)(mergedConfig);

dispatch(configDidLoad(config));
dispatch(authenticateUser());
})
.catch((err) => {
}
catch(err) {
dispatch(configFailed(err));
});
throw(err)
}
};
}
67 changes: 67 additions & 0 deletions src/bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Route } from 'react-router-dom';
import { ConnectedRouter } from 'react-router-redux';
import history from 'Routing/history';
import configureStore from 'Redux/configureStore';
import { mergeConfig } from 'Actions/config';
import { setStore } from 'ValueObjects/AssetProxy';
import { ErrorBoundary } from 'UI'
import App from 'App/App';
import 'EditorWidgets';
import 'MarkdownPlugins';
import './index.css';

function bootstrap({ config }) {
/**
* Log the version number.
*/
console.log(`Netlify CMS version ${NETLIFY_CMS_VERSION}`);

/**
* Create mount element dynamically.
*/
const el = document.createElement('div');
el.id = 'nc-root';
document.body.appendChild(el);

/**
* Configure Redux store.
*/
const store = configureStore();

/**
* Dispatch config to store if received. This config will be merged into
* config.yml if it exists, and any portion that produces a conflict will be
* overwritten.
*/
if (config) {
store.dispatch(mergeConfig(config));
}

/**
* Pass initial state into AssetProxy factory.
*/
setStore(store);

/**
* Create connected root component.
*/
const Root = () => (
<ErrorBoundary>
<Provider store={store}>
<ConnectedRouter history={history}>
<Route component={App}/>
</ConnectedRouter>
</Provider>
</ErrorBoundary>
);

/**
* Render application root.
*/
render(<Root />, el);
}

export default bootstrap;
2 changes: 1 addition & 1 deletion src/components/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class App extends React.Component {
<div>
<p>The <code>config.yml</code> file could not be loaded or failed to parse properly.</p>
<p><strong>Error message:</strong> {config.get('error')}</p>
<p>Check your console for details.</p>
</div>
</div>);
}
Expand Down Expand Up @@ -105,7 +106,6 @@ class App extends React.Component {
openMediaLibrary,
} = this.props;


if (config === null) {
return null;
}
Expand Down
Loading

0 comments on commit b61f41c

Please sign in to comment.