Feature(challenges): Load and cache required files

This commit is contained in:
Berkeley Martinez
2016-07-28 20:01:17 -07:00
parent 2d7e96045c
commit 2e9b179626
2 changed files with 48 additions and 10 deletions

View File

@ -21,22 +21,27 @@ function createFileStream(files = {}) {
); );
} }
const jQuery = { const globalRequires = [{
link: 'https://cdnjs.cloudflare.com/' +
'ajax/libs/normalize/4.2.0/normalize.min.css'
}, {
src: '/bower_components/jquery/dist/jquery.js', src: '/bower_components/jquery/dist/jquery.js',
script: true, script: true,
type: 'global' type: 'global',
}; crossDomain: false
}];
const scriptCache = new Map(); const scriptCache = new Map();
const linkCache = new Map();
function cacheScript({ src } = {}) { function cacheScript({ src } = {}, crossDomain = true) {
if (!src) { if (!src) {
return Observable.throw(new Error('No source provided for script')); return Observable.throw(new Error('No source provided for script'));
} }
if (scriptCache.has(src)) { if (scriptCache.has(src)) {
return scriptCache.get(src); return scriptCache.get(src);
} }
const script$ = ajax$(src) const script$ = ajax$({ url: src, crossDomain })
.doOnNext(res => { .doOnNext(res => {
if (res.status !== 200) { if (res.status !== 200) {
throw new Error('Request errror: ' + res.status); throw new Error('Request errror: ' + res.status);
@ -51,12 +56,37 @@ function cacheScript({ src } = {}) {
return script$; return script$;
} }
const frameRunner$ = cacheScript({ src: '/js/frame-runner.js' }); function cacheLink({ link } = {}, crossDomain = true) {
if (!link) {
return Observable.throw(new Error('No source provided for link'));
}
if (linkCache.has(link)) {
return linkCache.get(link);
}
const link$ = ajax$({ url: link, crossDomain })
.doOnNext(res => {
if (res.status !== 200) {
throw new Error('Request errror: ' + res.status);
}
})
.map(({ response }) => response)
.map(script => `<style>${script}</style>`)
.catch(createErrorObservable)
.shareReplay();
linkCache.set(link, link$);
return link$;
}
const htmlCatch = '\n<!--fcc-->'; const htmlCatch = '\n<!--fcc-->';
const jsCatch = '\n;/*fcc*/'; const jsCatch = '\n;/*fcc*/';
export default function executeChallengeSaga(action$, getState) { export default function executeChallengeSaga(action$, getState) {
const frameRunner$ = cacheScript(
{ src: '/js/frame-runner.js' },
false
);
return action$ return action$
.filter(({ type }) => ( .filter(({ type }) => (
type === types.executeChallenge || type === types.executeChallenge ||
@ -64,7 +94,8 @@ export default function executeChallengeSaga(action$, getState) {
)) ))
.debounce(750) .debounce(750)
.flatMapLatest(({ type }) => { .flatMapLatest(({ type }) => {
const { files, required = [ jQuery ] } = getState().challengesApp; const { files, required = [] } = getState().challengesApp;
const finalRequires = [...required, ...globalRequires ];
return createFileStream(files) return createFileStream(files)
::throwers() ::throwers()
::transformers() ::transformers()
@ -88,10 +119,13 @@ export default function executeChallengeSaga(action$, getState) {
}, '')) }, ''))
// add required scripts and links here // add required scripts and links here
.flatMap(source => { .flatMap(source => {
const head$ = Observable.from(required) const head$ = Observable.from(finalRequires)
.flatMap(required => { .flatMap(required => {
if (required.script) { if (required.script) {
return cacheScript(required); return cacheScript(required, required.crossDomain);
}
if (required.link) {
return cacheLink(required, required.crossDomain);
} }
return Observable.just(''); return Observable.just('');
}) })

View File

@ -15,7 +15,11 @@ if (process.env.NODE_ENV !== 'production') {
export default function csp() { export default function csp() {
return helmet.contentSecurityPolicy({ return helmet.contentSecurityPolicy({
directives: { directives: {
defaultSrc: trusted.concat('*.optimizely.com'), defaultSrc: trusted.concat([
'*.optimizely.com',
'https://*.cloudflare.com',
'*.cloudflare.com'
]),
scriptSrc: [ scriptSrc: [
"'unsafe-eval'", "'unsafe-eval'",
"'unsafe-inline'", "'unsafe-inline'",