Initial preview rendering

This commit is contained in:
Berkeley Martinez
2016-05-20 12:42:26 -07:00
parent 1db5caa701
commit 9b7bd2a026
24 changed files with 475 additions and 159 deletions

View File

@@ -6,6 +6,7 @@
} }
}, },
"env": { "env": {
"es6": true,
"browser": true, "browser": true,
"mocha": true, "mocha": true,
"node": true "node": true

1
.gitignore vendored
View File

@@ -44,6 +44,7 @@ public/js/main*
public/js/commonFramework* public/js/commonFramework*
public/js/sandbox* public/js/sandbox*
public/js/iFrameScripts* public/js/iFrameScripts*
public/js/frame-runner*
public/js/plugin* public/js/plugin*
public/js/vendor* public/js/vendor*
public/js/faux* public/js/faux*

70
client/frame-runner.js Normal file
View File

@@ -0,0 +1,70 @@
document.addEventListener('DOMContentLoaded', function() {
var common = parent.__common;
var Rx = parent.Rx;
common.getJsOutput = function evalJs(source = '') {
if (window.__err || !common.shouldRun()) {
return window.__err || 'source disabled';
}
let output;
try {
/* eslint-disable no-eval */
output = eval(source);
/* eslint-enable no-eval */
} catch (e) {
window.__err = e;
}
return output;
};
common.runTests$ = function runTests$({ tests = [], source }) {
const editor = { getValue() { return source; } };
if (window.__err) {
return Rx.Observable.throw(window.__err);
}
// Iterate through the test one at a time
// on new stacks
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
// add delay here for firefox to catch up
.delay(100)
.map(({ text, testString }) => {
const newTest = { text, testString };
let test;
try {
/* eslint-disable no-eval */
test = eval(testString);
/* eslint-enable no-eval */
if (typeof test === 'function') {
// maybe sync/promise/observable
if (test.length === 0) {
test();
}
// callback test
if (test.length === 1) {
console.log('callback test');
}
}
} catch (e) {
newTest.err = e.message.split(':').shift();
}
return newTest;
})
// gather tests back into an array
.toArray();
};
// used when updating preview without running tests
common.checkPreview$ = function checkPreview$(args) {
if (window.__err) {
return Rx.Observable.throw(window.__err);
}
return Rx.Observable.just(args);
};
// now that the runPreviewTest$ is defined
// we set the subject to true
// this will let the updatePreview
// script now that we are ready.
common.testFrameReady$.onNext(true);
});

View File

@@ -1,23 +1,15 @@
/* eslint-disable no-undef, no-unused-vars, no-native-reassign */ document.addEventListener('DOMContentLoaded', function() {
// the $ on the iframe window object is the same var common = parent.__common;
// as the one used on the main site, but
// uses the iframe document as the context
window.$(document).ready(function() {
var _ = parent._;
var Rx = parent.Rx; var Rx = parent.Rx;
var chai = parent.chai;
var assert = chai.assert;
var tests = parent.tests;
var common = parent.common;
common.getJsOutput = function evalJs(code = '') { common.getJsOutput = function evalJs(source = '') {
if (window.__err || !common.shouldRun()) { if (window.__err || !common.shouldRun()) {
return window.__err || 'code disabled'; return window.__err || 'source disabled';
} }
let output; let output;
try { try {
/* eslint-disable no-eval */ /* eslint-disable no-eval */
output = eval(code); output = eval(source);
/* eslint-enable no-eval */ /* eslint-enable no-eval */
} catch (e) { } catch (e) {
window.__err = e; window.__err = e;
@@ -25,48 +17,41 @@ window.$(document).ready(function() {
return output; return output;
}; };
common.runPreviewTests$ = common.runTests$ = function runTests$({ tests = [], source }) {
function runPreviewTests$({ const editor = { getValue() { return source; } };
tests = [],
originalCode,
...rest
}) {
const code = originalCode;
const editor = { getValue() { return originalCode; } };
if (window.__err) { if (window.__err) {
return Rx.Observable.throw(window.__err); return Rx.Observable.throw(window.__err);
} }
// Iterate throught the test one at a time // Iterate through the test one at a time
// on new stacks // on new stacks
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default) return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
// add delay here for firefox to catch up // add delay here for firefox to catch up
.delay(100) .delay(100)
.map(test => { .map(({ text, testString }) => {
const userTest = {}; const newTest = { text, testString };
let test;
try { try {
/* eslint-disable no-eval */ /* eslint-disable no-eval */
eval(test); test = eval(testString);
/* eslint-enable no-eval */ /* eslint-enable no-eval */
if (typeof test === 'function') {
// maybe sync/promise/observable
if (test.length === 0) {
test();
}
// callback test
if (test.length === 1) {
console.log('callback test');
}
}
} catch (e) { } catch (e) {
userTest.err = e.message.split(':').shift(); newTest.err = e.message.split(':').shift();
} finally {
if (!test.match(/message: /g)) {
// assumes test does not contain arrays
// This is a patch until all test fall into this pattern
userTest.text = test
.split(',')
.pop();
userTest.text = 'message: ' + userTest.text + '\');';
} else {
userTest.text = test;
} }
} return newTest;
return userTest;
}) })
// gather tests back into an array // gather tests back into an array
.toArray() .toArray();
.map(tests => ({ ...rest, tests, originalCode }));
}; };
// used when updating preview without running tests // used when updating preview without running tests
@@ -81,5 +66,5 @@ window.$(document).ready(function() {
// we set the subject to true // we set the subject to true
// this will let the updatePreview // this will let the updatePreview
// script now that we are ready. // script now that we are ready.
common.previewReady$.onNext(true); common.testFrameReady$.onNext(true);
}); });

View File

@@ -51,7 +51,7 @@ createApp({
serviceOptions, serviceOptions,
initialState, initialState,
middlewares: [ routingMiddleware ], middlewares: [ routingMiddleware ],
sagas, sagas: [...sagas ],
sagaOptions, sagaOptions,
reducers: { routing }, reducers: { routing },
enhancers: [ devTools ] enhancers: [ devTools ]

View File

@@ -1,21 +0,0 @@
import loopProtect from 'loopProtect';
loopProtect.hit = function hit(line) {
var err = 'Error: Exiting potential infinite loop at line ' +
line +
'. To disable loop protection, write: \n\\/\\/ noprotect\nas the first' +
'line. Beware that if you do have an infinite loop in your code' +
'this will crash your browser.';
console.error(err);
};
// Observable[Observable[File]]::addLoopProtect() => Observable[String]
export default function addLoopProtect() {
const source = this;
return source.map(files$ => files$.map(file => {
if (file.extname === 'js') {
file.contents = loopProtect(file.contents);
}
return file;
}));
}

View File

@@ -1,13 +0,0 @@
import createTypes from '../../common/app/utils/create-types';
const filterTypes = [
execute
];
export default function executeChallengeSaga(action$, getState) {
return action$
.filter(({ type }) => filterTypes.some(_type => _type === type))
.map(action => {
if (action.type === execute) {
const editors = getState().editors;
}
})
}

View File

@@ -1,14 +1,14 @@
import { helpers, Observable } from 'rx'; import { helpers, Observable } from 'rx';
const throwForJsHtml = { const throwForJsHtml = {
extname: /js|html/, ext: /js|html/,
throwers: [ throwers: [
{ {
name: 'multiline-comment', name: 'multiline-comment',
description: 'Detect if a JS multi-line comment is left open', description: 'Detect if a JS multi-line comment is left open',
thrower: function checkForComments({ content }) { thrower: function checkForComments({ contents }) {
const openingComments = content.match(/\/\*/gi); const openingComments = contents.match(/\/\*/gi);
const closingComments = content.match(/\*\//gi); const closingComments = contents.match(/\*\//gi);
if ( if (
openingComments && openingComments &&
(!closingComments || openingComments.length > closingComments.length) (!closingComments || openingComments.length > closingComments.length)
@@ -20,8 +20,8 @@ const throwForJsHtml = {
name: 'nested-jQuery', name: 'nested-jQuery',
description: 'Nested dollar sign calls breaks browsers', description: 'Nested dollar sign calls breaks browsers',
detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi, detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi,
thrower: function checkForNestedJquery({ content }) { thrower: function checkForNestedJquery({ contents }) {
if (content.match(this.detectUnsafeJQ)) { if (contents.match(this.detectUnsafeJQ)) {
throw new Error('Unsafe $($)'); throw new Error('Unsafe $($)');
} }
} }
@@ -29,10 +29,10 @@ const throwForJsHtml = {
name: 'unfinished-function', name: 'unfinished-function',
description: 'lonely function keywords breaks browsers', description: 'lonely function keywords breaks browsers',
detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi, detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi,
thower: function checkForUnfinishedFunction({ content: code }) { thrower: function checkForUnfinishedFunction({ contents }) {
if ( if (
code.match(/function/g) && contents.match(/function/g) &&
!code.match(this.detectFunctionCall) !contents.match(this.detectFunctionCall)
) { ) {
throw new Error( throw new Error(
'SyntaxError: Unsafe or unfinished function declaration' 'SyntaxError: Unsafe or unfinished function declaration'
@@ -43,8 +43,8 @@ const throwForJsHtml = {
name: 'unsafe console call', name: 'unsafe console call',
description: 'console call stops tests scripts from running', description: 'console call stops tests scripts from running',
detectUnsafeConsoleCall: /if\s\(null\)\sconsole\.log\(1\);/gi, detectUnsafeConsoleCall: /if\s\(null\)\sconsole\.log\(1\);/gi,
thrower: function checkForUnsafeConsole({ content }) { thrower: function checkForUnsafeConsole({ contents }) {
if (content.match(this.detectUnsafeConsoleCall)) { if (contents.match(this.detectUnsafeConsoleCall)) {
throw new Error('Invalid if (null) console.log(1); detected'); throw new Error('Invalid if (null) console.log(1); detected');
} }
} }
@@ -52,17 +52,17 @@ const throwForJsHtml = {
] ]
}; };
export default function pretester() { export default function throwers() {
const source = this; const source = this;
return source.map(file$ => file$.flatMap(file => { return source.map(file$ => file$.flatMap(file => {
if (!throwForJsHtml.extname.test(file.extname)) { if (!throwForJsHtml.ext.test(file.ext)) {
return Observable.just(file); return Observable.just(file);
} }
return Observable.from(throwForJsHtml.throwers) return Observable.from(throwForJsHtml.throwers)
.flatMap(({ thrower }) => { .flatMap(context => {
try { try {
let finalObs; let finalObs;
const maybeObservableOrPromise = thrower(file); const maybeObservableOrPromise = context.thrower(file);
if (helpers.isPromise(maybeObservableOrPromise)) { if (helpers.isPromise(maybeObservableOrPromise)) {
finalObs = Observable.fromPromise(maybeObservableOrPromise); finalObs = Observable.fromPromise(maybeObservableOrPromise);
} else if (Observable.isObservable(maybeObservableOrPromise)) { } else if (Observable.isObservable(maybeObservableOrPromise)) {

View File

@@ -0,0 +1,37 @@
import { Observable } from 'rx';
import loopProtect from 'loop-protect';
loopProtect.hit = function hit(line) {
var err = 'Error: Exiting potential infinite loop at line ' +
line +
'. To disable loop protection, write: \n\\/\\/ noprotect\nas the first' +
'line. Beware that if you do have an infinite loop in your code' +
'this will crash your browser.';
console.error(err);
};
const transformersForHtmlJS = {
ext: /html|js/,
transformers: [
{
name: 'add-loop-protect',
transformer: function addLoopProtect(file) {
file.contents = loopProtect(file.contents);
return file;
}
}
]
};
// Observable[Observable[File]]::addLoopProtect() => Observable[String]
export default function transformers() {
const source = this;
return source.map(files$ => files$.flatMap(file => {
if (!transformersForHtmlJS.ext.test(file.ext)) {
return Observable.just(file);
}
return Observable.from(transformersForHtmlJS.transformers)
.reduce((file, context) => context.transformer(file), file);
}));
}

View File

@@ -0,0 +1,98 @@
import { Observable } from 'rx';
import { ajax$ } from '../../common/utils/ajax-stream';
import throwers from '../new-framework/throwers';
import transformers from '../new-framework/transformers';
import types from '../../common/app/routes/challenges/redux/types';
import {
frameMain
} from '../../common/app/routes/challenges/redux/actions';
import { setExt, updateContents } from '../../common/utils/polyvinyl';
// createFileStream(files: Dictionary[Path, PolyVinyl]) =>
// Observable[...Observable[...PolyVinyl]]
function createFileStream(files = {}) {
return Observable.just(
Observable.from(Object.keys(files)).map(key => files[key])
);
}
const jQuery = {
src: '/bower_components/jquery/dist/jquery.js',
script: true,
type: 'global'
};
const scriptCache = new Map();
function cacheScript({ src } = {}) {
if (!src) {
return Observable.throw(new Error('No source provided for script'));
}
if (scriptCache.has(src)) {
return scriptCache.get(src);
}
const script$ = ajax$(src)
.doOnNext(res => {
console.log('status', res.status);
if (res.status !== 200) {
throw new Error('Request errror: ' + res.status);
}
})
.map(({ response }) => response)
.map(script => `<script>${script}</script>`)
.catch(e => (console.error(e), Observable.just('')))
.shareReplay();
scriptCache.set(src, script$);
return script$;
}
const frameRunner$ = cacheScript({ src: '/js/frame-runner.js' });
const htmlCatch = '\n<!-- -->';
const jsCatch = '\n;/* */';
export default function executeChallengeSaga(action$, getState) {
return action$
.filter(({ type }) => type === types.executeChallenge)
.debounce(750)
.flatMapLatest(() => {
const { files, required = [ jQuery ] } = getState().challengesApp;
return createFileStream(files)
::throwers()
::transformers()
// createbuild
.flatMap(file$ => file$.reduce((build, file) => {
let finalFile;
if (file.ext === 'js') {
finalFile = setExt('html', updateContents(
`<script>${file.contents}${jsCatch}</script>`,
file
));
} else if (file.ext === 'css') {
finalFile = setExt('html', updateContents(
`<style>${file.contents}</style>`,
file
));
} else {
finalFile = file;
}
return build + finalFile.contents + htmlCatch;
}, ''))
// add required scripts and links here
.flatMap(build => {
const header$ = Observable.from(required)
.flatMap(required => {
if (required.script) {
return cacheScript(required);
}
return Observable.just('');
})
.reduce((header, required) => header + required, '');
return Observable.combineLatest(header$, frameRunner$)
.map(([ header, frameRunner ]) => header + build + frameRunner);
})
.map(build => frameMain(build));
});
}

View File

@@ -0,0 +1,51 @@
import { BehaviorSubject } from 'rx';
import types from '../../common/app/routes/challenges/redux/types';
// we use three different frames to make them all essentially pure functions
const mainId = 'fcc-main-frame';
/*
const outputId = 'fcc-output-frame';
const testId = 'fcc-test-frame';
*/
function createFrame(document, id = mainId) {
const frame = document.createElement('iframe');
frame.id = id;
frame.setAttribute('style', 'display: none');
document.body.appendChild(frame);
return frame;
}
function refreshFrame(frame) {
frame.src = 'about:blank';
return frame;
}
function getFrameDocument(document, id = mainId) {
let frame = document.getElementById(id);
if (!frame) {
frame = createFrame(document, id);
} else {
refreshFrame(frame);
}
return frame.contentDocument || frame.contentWindow.document;
}
function frameMain(build, document) {
const main = getFrameDocument(document);
main.open();
main.write(build);
main.close();
}
export default function frameSaga(actions$, getState, { window, document }) {
window.__common = {};
window.__common.outputFrameReady$ = new BehaviorSubject(false);
window.__common.testFrameReady$ = new BehaviorSubject(false);
return actions$
.filter(({ type }) => type === types.frameMain)
.map(action => {
frameMain(action.payload, document);
return null;
});
}

View File

@@ -3,11 +3,15 @@ import titleSaga from './title-saga';
import localStorageSaga from './local-storage-saga'; import localStorageSaga from './local-storage-saga';
import hardGoToSaga from './hard-go-to-saga'; import hardGoToSaga from './hard-go-to-saga';
import windowSaga from './window-saga'; import windowSaga from './window-saga';
import executeChallengeSaga from './execute-challenge-saga';
import frameSaga from './frame-saga';
export default [ export default [
errSaga, errSaga,
titleSaga, titleSaga,
localStorageSaga, localStorageSaga,
hardGoToSaga, hardGoToSaga,
windowSaga windowSaga,
executeChallengeSaga,
frameSaga
]; ];

View File

@@ -8,23 +8,23 @@ import Editor from './Editor.jsx';
import SidePanel from './Side-Panel.jsx'; import SidePanel from './Side-Panel.jsx';
import Preview from './Preview.jsx'; import Preview from './Preview.jsx';
import { challengeSelector } from '../../redux/selectors'; import { challengeSelector } from '../../redux/selectors';
import { updateFile } from '../../redux/actions'; import { executeChallenge, updateFile } from '../../redux/actions';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
challengeSelector, challengeSelector,
state => state.challengesApp.tests, state => state.challengesApp.tests,
state => state.challengesApp.files, state => state.challengesApp.files,
state => state.challengesApp.path, state => state.challengesApp.key,
({ challenge, showPreview, mode }, tests, files = {}, path = '') => ({ ({ challenge, showPreview, mode }, tests, files = {}, key = '') => ({
content: files[path] && files[path].contents || '', content: files[key] && files[key].contents || '',
file: files[path], file: files[key],
showPreview, showPreview,
mode, mode,
tests tests
}) })
); );
const bindableActions = { updateFile }; const bindableActions = { executeChallenge, updateFile };
export class Challenge extends PureComponent { export class Challenge extends PureComponent {
static displayName = 'Challenge'; static displayName = 'Challenge';
@@ -32,7 +32,9 @@ export class Challenge extends PureComponent {
static propTypes = { static propTypes = {
showPreview: PropTypes.bool, showPreview: PropTypes.bool,
content: PropTypes.string, content: PropTypes.string,
mode: PropTypes.string mode: PropTypes.string,
updateFile: PropTypes.func,
executeChallenge: PropTypes.func
}; };
renderPreview(showPreview) { renderPreview(showPreview) {
@@ -54,7 +56,8 @@ export class Challenge extends PureComponent {
updateFile, updateFile,
file, file,
mode, mode,
showPreview showPreview,
executeChallenge
} = this.props; } = this.props;
return ( return (
<div> <div>
@@ -68,6 +71,7 @@ export class Challenge extends PureComponent {
md={ showPreview ? 5 : 8 }> md={ showPreview ? 5 : 8 }>
<Editor <Editor
content={ content } content={ content }
executeChallenge={ executeChallenge }
mode={ mode } mode={ mode }
updateFile={ content => updateFile(content, file) } updateFile={ content => updateFile(content, file) }
/> />

View File

@@ -36,6 +36,7 @@ export class Editor extends PureComponent {
} }
static displayName = 'Editor'; static displayName = 'Editor';
static propTypes = { static propTypes = {
executeChallenge: PropTypes.func,
height: PropTypes.number, height: PropTypes.number,
content: PropTypes.string, content: PropTypes.string,
mode: PropTypes.string, mode: PropTypes.string,
@@ -47,6 +48,40 @@ export class Editor extends PureComponent {
mode: 'javascript' mode: 'javascript'
}; };
createOptions = createSelector(
state => state.options,
state => state.executeChallenge,
state => state.mode,
(options, executeChallenge, mode) => ({
...options,
mode,
extraKeys: {
Tab(cm) {
if (cm.somethingSelected()) {
return cm.indentSelection('add');
}
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
return cm.replaceSelection(spaces);
},
'Shift-Tab': function(cm) {
if (cm.somethingSelected()) {
return cm.indentSelection('subtract');
}
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
return cm.replaceSelection(spaces);
},
'Ctrl-Enter': function() {
executeChallenge();
return false;
},
'Cmd-Enter': function() {
executeChallenge();
return false;
}
}
})
);
componentDidMount() { componentDidMount() {
const { updateFile = (() => {}) } = this.props; const { updateFile = (() => {}) } = this.props;
this._subscription = this._editorContent$ this._subscription = this._editorContent$
@@ -72,7 +107,7 @@ export class Editor extends PureComponent {
} }
render() { render() {
const { content, height, mode } = this.props; const { executeChallenge, content, height, mode } = this.props;
const style = {}; const style = {};
if (height) { if (height) {
style.height = height + 'px'; style.height = height + 'px';
@@ -84,7 +119,7 @@ export class Editor extends PureComponent {
<NoSSR> <NoSSR>
<Codemirror <Codemirror
onChange={ this.handleChange } onChange={ this.handleChange }
options={{ ...options, mode }} options={ this.createOptions({ executeChallenge, mode, options }) }
value={ content } /> value={ content } />
</NoSSR> </NoSSR>
</div> </div>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
const mainId = 'fcc-main-frame';
export default class extends PureComponent { export default class extends PureComponent {
static displayName = 'Preview'; static displayName = 'Preview';
@@ -14,7 +15,7 @@ export default class extends PureComponent {
</div> </div>
<iframe <iframe
className='iphone iframe-scroll' className='iphone iframe-scroll'
id='preview' /> id={ mainId } />
<div className='spacer' /> <div className='spacer' />
</div> </div>
); );

View File

@@ -1,17 +1,28 @@
import React from 'react'; import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Button, ButtonGroup } from 'react-bootstrap'; import { Button, ButtonGroup } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
export default class extends PureComponent { import { executeChallenge } from '../../redux/actions';
const bindableActions = { executeChallenge };
export class ToolPanel extends PureComponent {
static displayName = 'ToolPanel'; static displayName = 'ToolPanel';
static propTypes = {
executeChallenge: PropTypes.func
};
render() { render() {
const { executeChallenge } = this.props;
return ( return (
<div> <div>
<Button <Button
block={ true } block={ true }
bsStyle='primary' bsStyle='primary'
className='btn-big'> className='btn-big'
onClick={ executeChallenge }>
Run tests (ctrl + enter) Run tests (ctrl + enter)
</Button> </Button>
<div className='button-spacer' /> <div className='button-spacer' />
@@ -42,3 +53,5 @@ export default class extends PureComponent {
); );
} }
} }
export default connect(null, bindableActions)(ToolPanel);

View File

@@ -39,4 +39,11 @@ export const updateFile = createAction(
types.updateFile, types.updateFile,
(content, file) => updateContents(content, file) (content, file) => updateContents(content, file)
); );
export const updateFiles = createAction(types.updateFiles); export const updateFiles = createAction(types.updateFiles);
// rechallenge
export const executeChallenge = createAction(types.executeChallenge);
export const frameMain = createAction(types.frameMain);
export const frameOutput = createAction(types.frameOutput);
export const frameTests = createAction(types.frameTests);

View File

@@ -3,7 +3,13 @@ import { createPoly } from '../../../../utils/polyvinyl';
import types from './types'; import types from './types';
import { BONFIRE, HTML, JS } from '../../../utils/challengeTypes'; import { BONFIRE, HTML, JS } from '../../../utils/challengeTypes';
import { arrayToString, buildSeed, createTests, getPath } from '../utils'; import {
arrayToString,
buildSeed,
createTests,
getPreFile,
getFileKey
} from '../utils';
const initialState = { const initialState = {
challenge: '', challenge: '',
@@ -24,7 +30,7 @@ const mainReducer = handleActions(
...state, ...state,
refresh: true, refresh: true,
challenge: challenge.dashedName, challenge: challenge.dashedName,
path: getPath(challenge), key: getFileKey(challenge),
tests: createTests(challenge) tests: createTests(challenge)
}), }),
@@ -57,12 +63,12 @@ const filesReducer = handleActions(
{ {
[types.updateFile]: (state, { payload: file }) => ({ [types.updateFile]: (state, { payload: file }) => ({
...state, ...state,
[file.path]: file [file.key]: file
}), }),
[types.updateFiles]: (state, { payload: files }) => { [types.updateFiles]: (state, { payload: files }) => {
return files return files
.reduce((files, file) => { .reduce((files, file) => {
files[file.path] = file; files[file.key] = file;
return files; return files;
}, { ...state }); }, { ...state });
}, },
@@ -77,11 +83,11 @@ const filesReducer = handleActions(
) { ) {
return {}; return {};
} }
const path = getPath(challenge); const preFile = getPreFile(challenge);
return { return {
...state, ...state,
[path]: createPoly({ [preFile.key]: createPoly({
path, ...preFile,
contents: buildSeed(challenge), contents: buildSeed(challenge),
head: arrayToString(challenge.head), head: arrayToString(challenge.head),
tail: arrayToString(challenge.tail) tail: arrayToString(challenge.tail)

View File

@@ -17,5 +17,11 @@ export default createTypes([
// files // files
'updateFile', 'updateFile',
'updateFiles' 'updateFiles',
// rechallenge
'executeChallenge',
'frameMain',
'frameOutput',
'frameTests'
], 'challenges'); ], 'challenges');

View File

@@ -40,13 +40,21 @@ export function buildSeed({ challengeSeed = [] } = {}) {
} }
const pathsMap = { const pathsMap = {
[HTML]: 'main.html', [HTML]: 'html',
[JS]: 'main.js', [JS]: 'js',
[BONFIRE]: 'main.js' [BONFIRE]: 'js'
}; };
export function getPath({ challengeType }) { export function getPreFile({ challengeType }) {
return pathsMap[challengeType] || 'main'; return {
name: 'index',
ext: pathsMap[challengeType] || 'html',
key: getFileKey({ challengeType })
};
}
export function getFileKey({ challengeType }) {
return 'index' + (pathsMap[challengeType] || 'html');
} }
export function createTests({ tests = [] }) { export function createTests({ tests = [] }) {

View File

@@ -1,25 +1,41 @@
// originally base off of https://github.com/gulpjs/vinyl // originally base off of https://github.com/gulpjs/vinyl
import path from 'path';
import replaceExt from 'replace-ext';
import invariant from 'invariant'; import invariant from 'invariant';
// interface PolyVinyl { // interface PolyVinyl {
// contents: String, // contents: String,
// name: String,
// ext: String,
// path: String, // path: String,
// key: String,
// head: String,
// tail: String,
// history: [...String], // history: [...String],
// error: Null|Object // error: Null|Object
// } // }
// //
// createPoly({ // createPoly({
// path: String, // name: String,
// ext: String,
// contents: String, // contents: String,
// history?: [...String], // history?: [...String],
// }) => PolyVinyl, throws // }) => PolyVinyl, throws
export function createPoly({ path, contents, history, ...rest } = {}) { export function createPoly({
name,
ext,
contents,
history,
...rest
} = {}) {
invariant( invariant(
typeof path === 'string', typeof name === 'string',
'path must be a string but got %s', 'name must be a string but got %s',
path name
);
invariant(
typeof ext === 'string',
'ext must be a string, but was %s',
ext
); );
invariant( invariant(
@@ -30,9 +46,12 @@ export function createPoly({ path, contents, history, ...rest } = {}) {
return { return {
...rest, ...rest,
history: Array.isArray(history) ? history : [ path ], history: Array.isArray(history) ? history : [ name + ext ],
path: path, name,
contents: contents, ext,
path: name + '.' + ext,
key: name + ext,
contents,
error: null error: null
}; };
} }
@@ -41,7 +60,8 @@ export function createPoly({ path, contents, history, ...rest } = {}) {
export function isPoly(poly) { export function isPoly(poly) {
return poly && return poly &&
typeof poly.contents === 'string' && typeof poly.contents === 'string' &&
typeof poly.path === 'string' && typeof poly.name === 'string' &&
typeof poly.ext === 'string' &&
Array.isArray(poly.history); Array.isArray(poly.history);
} }
@@ -69,23 +89,25 @@ export function updateContents(contents, poly) {
}; };
} }
export function getExt(poly) { export function setExt(ext, poly) {
checkPoly(poly); checkPoly(poly);
invariant(
!!poly.path,
'No path specified! Can not get extname'
);
return path.extname(poly.path);
}
export function setExt(extname, poly) {
invariant(
poly.path,
'No path specified! Can not set extname',
);
const newPoly = { const newPoly = {
...poly, ...poly,
path: replaceExt(this.path, extname) ext,
path: poly.name + '.' + ext,
key: poly.name + ext
};
newPoly.history = [ ...poly.history, newPoly.path ];
return newPoly;
}
export function setName(name, poly) {
checkPoly(poly);
const newPoly = {
...poly,
name,
path: name + '.' + poly.ext,
key: name + poly.ext
}; };
newPoly.history = [ ...poly.history, newPoly.path ]; newPoly.history = [ ...poly.history, newPoly.path ];
return newPoly; return newPoly;

View File

@@ -113,6 +113,7 @@ var paths = {
js: [ js: [
'client/main.js', 'client/main.js',
'client/iFrameScripts.js', 'client/iFrameScripts.js',
'client/frame-runner.js',
'client/plugin.js' 'client/plugin.js'
], ],

View File

@@ -98,7 +98,6 @@
"redux-actions": "^0.9.1", "redux-actions": "^0.9.1",
"redux-epic": "^0.1.1", "redux-epic": "^0.1.1",
"redux-form": "^5.2.3", "redux-form": "^5.2.3",
"replace-ext": "0.0.1",
"request": "^2.65.0", "request": "^2.65.0",
"reselect": "^2.0.2", "reselect": "^2.0.2",
"rx": "^4.0.0", "rx": "^4.0.0",

View File

@@ -39,7 +39,8 @@ module.exports = {
] ]
}, },
externals: { externals: {
codemirror: 'CodeMirror' codemirror: 'CodeMirror',
'loop-protect': 'loopProtect'
}, },
plugins: [ plugins: [
new webpack.optimize.DedupePlugin(), new webpack.optimize.DedupePlugin(),