-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
allow manual initialization and config as code #1149
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,7 +41,6 @@ | |
"setupFiles": [ | ||
"./setupTests.js" | ||
], | ||
"mapCoverage": true, | ||
"coverageReporters": [ | ||
"lcov" | ||
], | ||
|
@@ -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", | ||
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Jest updated for fix of |
||
"npm-check": "^5.2.3", | ||
"postcss-cssnext": "^3.0.2", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,117 +1,120 @@ | ||
import { fromJS } from 'immutable'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Causes for change in this file:
|
||
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.'); | ||
}); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,63 @@ | ||
import yaml from "js-yaml"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Causes for change in this file:
|
||
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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allows configuration to be merged into the state without triggering |
||
|
||
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]) { | ||
|
@@ -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) | ||
} | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import React from 'react'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is simply a paste of the core application initialization code that used to be in |
||
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer required in Jest 22.