allow manual initialization and config as an arg
This commit is contained in:
parent
95b6d8a884
commit
a83c04cad0
@ -43,7 +43,6 @@
|
|||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"./setupTests.js"
|
"./setupTests.js"
|
||||||
],
|
],
|
||||||
"mapCoverage": true,
|
|
||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
"lcov"
|
"lcov"
|
||||||
],
|
],
|
||||||
@ -77,7 +76,7 @@
|
|||||||
"babel": "^6.5.2",
|
"babel": "^6.5.2",
|
||||||
"babel-cli": "^6.18.0",
|
"babel-cli": "^6.18.0",
|
||||||
"babel-core": "^6.23.1",
|
"babel-core": "^6.23.1",
|
||||||
"babel-jest": "^21.2.0",
|
"babel-jest": "^22.0.0",
|
||||||
"babel-loader": "^7.0.0",
|
"babel-loader": "^7.0.0",
|
||||||
"babel-plugin-lodash": "^3.2.0",
|
"babel-plugin-lodash": "^3.2.0",
|
||||||
"babel-plugin-module-resolver": "^3.0.0",
|
"babel-plugin-module-resolver": "^3.0.0",
|
||||||
@ -104,8 +103,8 @@
|
|||||||
"file-loader": "^1.1.4",
|
"file-loader": "^1.1.4",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"imports-loader": "^0.7.1",
|
"imports-loader": "^0.7.1",
|
||||||
"jest": "^21.2.1",
|
"jest": "^22.0.0",
|
||||||
"jest-cli": "^21.2.1",
|
"jest-cli": "^22.0.0",
|
||||||
"lint-staged": "^3.3.1",
|
"lint-staged": "^3.3.1",
|
||||||
"npm-check": "^5.2.3",
|
"npm-check": "^5.2.3",
|
||||||
"postcss-cssnext": "^3.0.2",
|
"postcss-cssnext": "^3.0.2",
|
||||||
|
@ -1,61 +1,64 @@
|
|||||||
|
import { fromJS } from 'immutable';
|
||||||
import { applyDefaults, validateConfig } from '../config';
|
import { applyDefaults, validateConfig } from '../config';
|
||||||
|
|
||||||
describe('config', () => {
|
describe('config', () => {
|
||||||
describe('applyDefaults', () => {
|
describe('applyDefaults', () => {
|
||||||
it('should set publish_mode if not set', () => {
|
it('should set publish_mode if not set', () => {
|
||||||
expect(applyDefaults({
|
const config = fromJS({
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
media_folder: 'path/to/media',
|
media_folder: 'path/to/media',
|
||||||
})).toEqual({
|
|
||||||
foo: 'bar',
|
|
||||||
publish_mode: 'simple',
|
|
||||||
media_folder: 'path/to/media',
|
|
||||||
public_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', () => {
|
it('should set publish_mode from config', () => {
|
||||||
expect(applyDefaults({
|
const config = fromJS({
|
||||||
foo: 'bar',
|
|
||||||
publish_mode: 'complex',
|
|
||||||
media_folder: 'path/to/media',
|
|
||||||
})).toEqual({
|
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
publish_mode: 'complex',
|
publish_mode: 'complex',
|
||||||
media_folder: 'path/to/media',
|
media_folder: 'path/to/media',
|
||||||
public_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', () => {
|
it('should set public_folder based on media_folder if not set', () => {
|
||||||
expect(applyDefaults({
|
expect(applyDefaults(fromJS({
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
media_folder: 'path/to/media',
|
media_folder: 'path/to/media',
|
||||||
})).toEqual({
|
}))).toEqual(fromJS({
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
publish_mode: 'simple',
|
publish_mode: 'simple',
|
||||||
media_folder: 'path/to/media',
|
media_folder: 'path/to/media',
|
||||||
public_folder: '/path/to/media',
|
public_folder: '/path/to/media',
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not overwrite public_folder if set', () => {
|
it('should not overwrite public_folder if set', () => {
|
||||||
expect(applyDefaults({
|
expect(applyDefaults(fromJS({
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
media_folder: 'path/to/media',
|
media_folder: 'path/to/media',
|
||||||
public_folder: '/publib/path',
|
public_folder: '/publib/path',
|
||||||
})).toEqual({
|
}))).toEqual(fromJS({
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
publish_mode: 'simple',
|
publish_mode: 'simple',
|
||||||
media_folder: 'path/to/media',
|
media_folder: 'path/to/media',
|
||||||
public_folder: '/publib/path',
|
public_folder: '/publib/path',
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateConfig', () => {
|
describe('validateConfig', () => {
|
||||||
it('should return the config if no errors', () => {
|
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(
|
expect(
|
||||||
validateConfig(config)
|
validateConfig(config)
|
||||||
).toEqual(config);
|
).toEqual(config);
|
||||||
@ -63,55 +66,55 @@ describe('config', () => {
|
|||||||
|
|
||||||
it('should throw if backend is not defined in config', () => {
|
it('should throw if backend is not defined in config', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
validateConfig({ foo: 'bar' });
|
validateConfig(fromJS({ foo: 'bar' }));
|
||||||
}).toThrowError('Error in configuration file: A `backend` wasn\'t found. Check your config.yml file.');
|
}).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', () => {
|
it('should throw if backend name is not defined in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).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', () => {
|
it('should throw if backend name is not a string in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).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', () => {
|
it('should throw if media_folder is not defined in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).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', () => {
|
it('should throw if media_folder is not a string in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).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', () => {
|
it('should throw if collections is not defined in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).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', () => {
|
it('should throw if collections not an array in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).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', () => {
|
it('should throw if collections is an empty array in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).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', () => {
|
it('should throw if collections is an array with a single null element in config', () => {
|
||||||
expect(() => {
|
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.');
|
}).toThrowError('Error in configuration file: Your `collections` must be an array with at least one element. Check your config.yml file.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,50 +1,63 @@
|
|||||||
import yaml from "js-yaml";
|
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 { authenticateUser } from "Actions/auth";
|
||||||
import * as publishModes from "Constants/publishModes";
|
import * as publishModes from "Constants/publishModes";
|
||||||
|
|
||||||
export const CONFIG_REQUEST = "CONFIG_REQUEST";
|
export const CONFIG_REQUEST = "CONFIG_REQUEST";
|
||||||
export const CONFIG_SUCCESS = "CONFIG_SUCCESS";
|
export const CONFIG_SUCCESS = "CONFIG_SUCCESS";
|
||||||
export const CONFIG_FAILURE = "CONFIG_FAILURE";
|
export const CONFIG_FAILURE = "CONFIG_FAILURE";
|
||||||
|
export const CONFIG_MERGE = "CONFIG_MERGE";
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
publish_mode: publishModes.SIMPLE,
|
publish_mode: publishModes.SIMPLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function applyDefaults(config) {
|
export function applyDefaults(config) {
|
||||||
// Make sure there is a public folder
|
return Map(defaults)
|
||||||
set(defaults,
|
.mergeDeep(config)
|
||||||
"public_folder",
|
.withMutations(map => {
|
||||||
config.media_folder.charAt(0) === "/" ? config.media_folder : `/${ config.media_folder }`);
|
/**
|
||||||
|
* Use media_folder as default public_folder.
|
||||||
return defaultsDeep(config, defaults);
|
*/
|
||||||
|
const defaultPublicFolder = `/${trimStart(map.get('media_folder'), '/')}`;
|
||||||
|
if (!map.get('public_folder')) {
|
||||||
|
map.set('public_folder', defaultPublicFolder);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateConfig(config) {
|
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.");
|
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.");
|
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.");
|
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.");
|
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.");
|
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.");
|
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.");
|
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;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergePreloadedConfig(preloadedConfig, loadedConfig) {
|
||||||
|
const map = fromJS(loadedConfig) || Map();
|
||||||
|
return preloadedConfig ? preloadedConfig.mergeDeep(map) : map;
|
||||||
|
}
|
||||||
|
|
||||||
function parseConfig(data) {
|
function parseConfig(data) {
|
||||||
const config = yaml.safeLoad(data);
|
const config = yaml.safeLoad(data);
|
||||||
if (typeof CMS_ENV === "string" && config[CMS_ENV]) {
|
if (typeof CMS_ENV === "string" && config[CMS_ENV]) {
|
||||||
@ -82,29 +95,40 @@ export function configDidLoad(config) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeConfig(config) {
|
||||||
|
return { type: CONFIG_MERGE, payload: config };
|
||||||
|
}
|
||||||
|
|
||||||
export function loadConfig() {
|
export function loadConfig() {
|
||||||
if (window.CMS_CONFIG) {
|
if (window.CMS_CONFIG) {
|
||||||
return configDidLoad(window.CMS_CONFIG);
|
return configDidLoad(fromJS(window.CMS_CONFIG));
|
||||||
}
|
}
|
||||||
return (dispatch) => {
|
return async (dispatch, getState) => {
|
||||||
dispatch(configLoading());
|
dispatch(configLoading());
|
||||||
|
|
||||||
fetch("config.yml", { credentials: 'same-origin' })
|
try {
|
||||||
.then((response) => {
|
const preloadedConfig = getState().config;
|
||||||
if (response.status !== 200) {
|
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 })`);
|
throw new Error(`Failed to load config.yml (${ response.status })`);
|
||||||
}
|
}
|
||||||
return response.text();
|
|
||||||
})
|
const loadedConfig = parseConfig(requestSuccess ? await response.text() : '');
|
||||||
.then(parseConfig)
|
|
||||||
.then(validateConfig)
|
/**
|
||||||
.then(applyDefaults)
|
* Merge any existing configuration so the result can be validated.
|
||||||
.then((config) => {
|
*/
|
||||||
|
const mergedConfig = mergePreloadedConfig(preloadedConfig, loadedConfig)
|
||||||
|
const config = flow(validateConfig, applyDefaults)(mergedConfig);
|
||||||
|
|
||||||
dispatch(configDidLoad(config));
|
dispatch(configDidLoad(config));
|
||||||
dispatch(authenticateUser());
|
dispatch(authenticateUser());
|
||||||
})
|
}
|
||||||
.catch((err) => {
|
catch(err) {
|
||||||
dispatch(configFailed(err));
|
dispatch(configFailed(err));
|
||||||
});
|
throw(err)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
67
src/bootstrap.js
vendored
Normal file
67
src/bootstrap.js
vendored
Normal file
@ -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;
|
@ -53,6 +53,7 @@ class App extends React.Component {
|
|||||||
<div>
|
<div>
|
||||||
<p>The <code>config.yml</code> file could not be loaded or failed to parse properly.</p>
|
<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><strong>Error message:</strong> {config.get('error')}</p>
|
||||||
|
<p>Check your console for details.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
@ -105,7 +106,6 @@ class App extends React.Component {
|
|||||||
openMediaLibrary,
|
openMediaLibrary,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
|
||||||
if (config === null) {
|
if (config === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
54
src/index.js
54
src/index.js
@ -1,54 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* This module provides a self-initializing CMS instance with API hooks added to
|
||||||
|
* the `window` object.
|
||||||
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
import bootstrap from './bootstrap';
|
||||||
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 { setStore } from 'ValueObjects/AssetProxy';
|
|
||||||
import { ErrorBoundary } from 'UI'
|
|
||||||
import registry from 'Lib/registry';
|
import registry from 'Lib/registry';
|
||||||
import App from 'App/App';
|
import createReactClass from 'create-react-class';
|
||||||
import 'EditorWidgets';
|
|
||||||
import 'MarkdownPlugins';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the version number.
|
* Load the app.
|
||||||
*/
|
*/
|
||||||
console.log(`Netlify CMS version ${NETLIFY_CMS_VERSION}`);
|
bootstrap();
|
||||||
|
|
||||||
/**
|
|
||||||
* Create mount element dynamically.
|
|
||||||
*/
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.id = 'nc-root';
|
|
||||||
document.body.appendChild(el);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure Redux store.
|
|
||||||
*/
|
|
||||||
const store = configureStore();
|
|
||||||
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);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add extension hooks to global scope.
|
* Add extension hooks to global scope.
|
||||||
|
7
src/init.js
Normal file
7
src/init.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* This module provides manual initialization and registry hooks.
|
||||||
|
*/
|
||||||
|
import bootstrap from './bootstrap';
|
||||||
|
import registry from 'Lib/registry';
|
||||||
|
|
||||||
|
export { bootstrap as init, registry };
|
@ -13,7 +13,7 @@ describe('collections', () => {
|
|||||||
|
|
||||||
it('should load the collections from the config', () => {
|
it('should load the collections from the config', () => {
|
||||||
expect(
|
expect(
|
||||||
collections(undefined, configLoaded({
|
collections(undefined, configLoaded(fromJS({
|
||||||
collections: [
|
collections: [
|
||||||
{
|
{
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
@ -21,7 +21,7 @@ describe('collections', () => {
|
|||||||
fields: [{ name: 'title', widget: 'string' }],
|
fields: [{ name: 'title', widget: 'string' }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}))
|
})))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
OrderedMap({
|
OrderedMap({
|
||||||
posts: fromJS({
|
posts: fromJS({
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import Immutable from 'immutable';
|
import { Map } from 'immutable';
|
||||||
import { configLoaded, configLoading, configFailed } from 'Actions/config';
|
import { configLoaded, configLoading, configFailed } from 'Actions/config';
|
||||||
import config from '../config';
|
import config from 'Reducers/config';
|
||||||
|
|
||||||
describe('config', () => {
|
describe('config', () => {
|
||||||
it('should handle an empty state', () => {
|
it('should handle an empty state', () => {
|
||||||
expect(
|
expect(
|
||||||
config(undefined, {})
|
config(undefined, {})
|
||||||
).toEqual(
|
).toEqual(
|
||||||
null
|
Map({ isFetching: true })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an update', () => {
|
it('should handle an update', () => {
|
||||||
expect(
|
expect(
|
||||||
config(Immutable.Map({ a: 'b', c: 'd' }), configLoaded({ a: 'changed', e: 'new' }))
|
config(Map({ a: 'b', c: 'd' }), configLoaded(Map({ a: 'changed', e: 'new' })))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
Immutable.Map({ a: 'changed', e: 'new' })
|
Map({ a: 'changed', e: 'new' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,15 +23,15 @@ describe('config', () => {
|
|||||||
expect(
|
expect(
|
||||||
config(undefined, configLoading())
|
config(undefined, configLoading())
|
||||||
).toEqual(
|
).toEqual(
|
||||||
Immutable.Map({ isFetching: true })
|
Map({ isFetching: true })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an error', () => {
|
it('should handle an error', () => {
|
||||||
expect(
|
expect(
|
||||||
config(Immutable.Map({ isFetching: true }), configFailed(new Error('Config could not be loaded')))
|
config(Map(), configFailed(new Error('Config could not be loaded')))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
Immutable.Map({ error: 'Error: Config could not be loaded' })
|
Map({ error: 'Error: Config could not be loaded' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { OrderedMap, fromJS } from 'immutable';
|
import { List } from 'immutable';
|
||||||
import { has, get, escapeRegExp } from 'lodash';
|
import { has, get, escapeRegExp } from 'lodash';
|
||||||
import consoleError from 'Lib/consoleError';
|
import consoleError from 'Lib/consoleError';
|
||||||
import { CONFIG_SUCCESS } from 'Actions/config';
|
import { CONFIG_SUCCESS } from 'Actions/config';
|
||||||
@ -7,42 +7,50 @@ import { INFERABLE_FIELDS } from 'Constants/fieldInference';
|
|||||||
import { formatByExtension, formatToExtension, supportedFormats, frontmatterFormats } from 'Formats/formats';
|
import { formatByExtension, formatToExtension, supportedFormats, frontmatterFormats } from 'Formats/formats';
|
||||||
|
|
||||||
const collections = (state = null, action) => {
|
const collections = (state = null, action) => {
|
||||||
const configCollections = action.payload && action.payload.collections;
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CONFIG_SUCCESS:
|
case CONFIG_SUCCESS:
|
||||||
return OrderedMap().withMutations((map) => {
|
const configCollections = action.payload ? action.payload.get('collections') : List();
|
||||||
(configCollections || []).forEach((configCollection) => {
|
configCollections.forEach(validateCollection)
|
||||||
validateCollection(configCollection);
|
return configCollections
|
||||||
if (has(configCollection, 'folder')) {
|
.toOrderedMap()
|
||||||
configCollection.type = FOLDER; // eslint-disable-line no-param-reassign
|
.map(collection => {
|
||||||
} else if (has(configCollection, 'files')) {
|
if (collection.has('folder')) {
|
||||||
configCollection.type = FILES; // eslint-disable-line no-param-reassign
|
return collection.set('type', FOLDER);
|
||||||
}
|
}
|
||||||
map.set(configCollection.name, fromJS(configCollection));
|
if (collection.has('files')) {
|
||||||
});
|
return collection.set('type', FILES);
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.mapKeys((key, collection) => collection.get('name'));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function validateCollection(configCollection) {
|
function validateCollection(configCollection) {
|
||||||
const collectionName = get(configCollection, 'name');
|
const {
|
||||||
if (!has(configCollection, 'folder') && !has(configCollection, 'files')) {
|
name,
|
||||||
throw new Error(`Unknown collection type for collection "${ collectionName }". Collections can be either Folder based or File based.`);
|
folder,
|
||||||
}
|
files,
|
||||||
if (has(configCollection, 'format') && !supportedFormats.includes(get(configCollection, 'format'))) {
|
format,
|
||||||
throw new Error(`Unknown collection format for collection "${ collectionName }". Supported formats are ${ supportedFormats.join(',') }`);
|
extension,
|
||||||
}
|
frontmatter_delimiter: delimiter
|
||||||
if (!has(configCollection, 'format') && has(configCollection, 'extension') && !formatByExtension(get(configCollection, 'extension'))) {
|
} = configCollection.toJS();
|
||||||
// Cannot infer format from extension.
|
|
||||||
throw new Error(`Please set a format for collection "${ collectionName }". Supported formats are ${ supportedFormats.join(',') }`);
|
|
||||||
}
|
|
||||||
if (has(configCollection, 'frontmatter_delimiter') && !frontmatterFormats.includes(get(configCollection, 'format'))) {
|
|
||||||
// Cannot set custom delimiter without explicit and proper frontmatter format declaration
|
|
||||||
throw new Error(`Please set a proper frontmatter format for collection "${ collectionName }" to use a custom delimiter. Supported frontmatter formats are yaml-frontmatter, toml-frontmatter, and json-frontmatter.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!folder && !files) {
|
||||||
|
throw new Error(`Unknown collection type for collection "${name}". Collections can be either Folder based or File based.`);
|
||||||
|
}
|
||||||
|
if (format && !supportedFormats.includes(format)) {
|
||||||
|
throw new Error(`Unknown collection format for collection "${name}". Supported formats are ${supportedFormats.join(',')}`);
|
||||||
|
}
|
||||||
|
if (!format && extension && !formatByExtension(extension)) {
|
||||||
|
// Cannot infer format from extension.
|
||||||
|
throw new Error(`Please set a format for collection "${name}". Supported formats are ${supportedFormats.join(',')}`);
|
||||||
|
}
|
||||||
|
if (delimiter && !frontmatterFormats.includes(format)) {
|
||||||
|
// Cannot set custom delimiter without explicit and proper frontmatter format declaration
|
||||||
|
throw new Error(`Please set a proper frontmatter format for collection "${name}" to use a custom delimiter. Supported frontmatter formats are yaml-frontmatter, toml-frontmatter, and json-frontmatter.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectors = {
|
const selectors = {
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
import Immutable from 'immutable';
|
import { Map } from 'immutable';
|
||||||
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from 'Actions/config';
|
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE, CONFIG_MERGE } from 'Actions/config';
|
||||||
|
|
||||||
const config = (state = null, action) => {
|
const config = (state = Map({ isFetching: true }), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case CONFIG_MERGE:
|
||||||
|
return state.mergeDeep(action.payload);
|
||||||
case CONFIG_REQUEST:
|
case CONFIG_REQUEST:
|
||||||
return Immutable.Map({ isFetching: true });
|
return state.set('isFetching', true);
|
||||||
case CONFIG_SUCCESS:
|
case CONFIG_SUCCESS:
|
||||||
return Immutable.fromJS(action.payload);
|
/**
|
||||||
|
* The loadConfig action merges any existing config into the loaded config
|
||||||
|
* before firing this action (so the resulting config can be validated),
|
||||||
|
* so we don't have to merge it here.
|
||||||
|
*/
|
||||||
|
return action.payload.delete('isFetching');
|
||||||
case CONFIG_FAILURE:
|
case CONFIG_FAILURE:
|
||||||
return Immutable.Map({ error: action.payload.toString() });
|
return Map({ error: action.payload.toString() });
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { fromJS } from 'immutable';
|
import { fromJS, List } from 'immutable';
|
||||||
import { CONFIG_SUCCESS } from 'Actions/config';
|
import { CONFIG_SUCCESS } from 'Actions/config';
|
||||||
|
|
||||||
const integrations = (state = null, action) => {
|
const integrations = (state = null, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CONFIG_SUCCESS:
|
case CONFIG_SUCCESS:
|
||||||
const integrations = action.payload.integrations || [];
|
const integrations = action.payload.get('integrations').toJS() || [];
|
||||||
const newState = integrations.reduce((acc, integration) => {
|
const newState = integrations.reduce((acc, integration) => {
|
||||||
const { hooks, collections, provider, ...providerData } = integration;
|
const { hooks, collections, provider, ...providerData } = integration;
|
||||||
acc.providers[provider] = { ...providerData };
|
acc.providers[provider] = { ...providerData };
|
||||||
|
@ -13,6 +13,7 @@ module.exports = merge.smart(require('./webpack.base.js'), {
|
|||||||
`webpack-dev-server/client?http://${ HOST }:${ PORT }/`,
|
`webpack-dev-server/client?http://${ HOST }:${ PORT }/`,
|
||||||
'./index',
|
'./index',
|
||||||
],
|
],
|
||||||
|
init: './init',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
|
@ -8,6 +8,7 @@ const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
|||||||
module.exports = merge.smart(require('./webpack.base.js'), {
|
module.exports = merge.smart(require('./webpack.base.js'), {
|
||||||
entry: {
|
entry: {
|
||||||
cms: './index',
|
cms: './index',
|
||||||
|
init: './init',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user