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": {
"es6": true,
"browser": true,
"mocha": true,
"node": true

1
.gitignore vendored
View File

@@ -44,6 +44,7 @@ public/js/main*
public/js/commonFramework*
public/js/sandbox*
public/js/iFrameScripts*
public/js/frame-runner*
public/js/plugin*
public/js/vendor*
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 */
// the $ on the iframe window object is the same
// as the one used on the main site, but
// uses the iframe document as the context
window.$(document).ready(function() {
var _ = parent._;
document.addEventListener('DOMContentLoaded', function() {
var common = parent.__common;
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()) {
return window.__err || 'code disabled';
return window.__err || 'source disabled';
}
let output;
try {
/* eslint-disable no-eval */
output = eval(code);
output = eval(source);
/* eslint-enable no-eval */
} catch (e) {
window.__err = e;
@@ -25,49 +17,42 @@ window.$(document).ready(function() {
return output;
};
common.runPreviewTests$ =
function runPreviewTests$({
tests = [],
originalCode,
...rest
}) {
const code = originalCode;
const editor = { getValue() { return originalCode; } };
if (window.__err) {
return Rx.Observable.throw(window.__err);
}
common.runTests$ = function runTests$({ tests = [], source }) {
const editor = { getValue() { return source; } };
if (window.__err) {
return Rx.Observable.throw(window.__err);
}
// Iterate throught 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(test => {
const userTest = {};
try {
/* eslint-disable no-eval */
eval(test);
/* eslint-enable no-eval */
} catch (e) {
userTest.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;
// 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');
}
}
return userTest;
})
// gather tests back into an array
.toArray()
.map(tests => ({ ...rest, tests, originalCode }));
};
} 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) {
@@ -81,5 +66,5 @@ window.$(document).ready(function() {
// we set the subject to true
// this will let the updatePreview
// script now that we are ready.
common.previewReady$.onNext(true);
common.testFrameReady$.onNext(true);
});

View File

@@ -51,7 +51,7 @@ createApp({
serviceOptions,
initialState,
middlewares: [ routingMiddleware ],
sagas,
sagas: [...sagas ],
sagaOptions,
reducers: { routing },
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';
const throwForJsHtml = {
extname: /js|html/,
ext: /js|html/,
throwers: [
{
name: 'multiline-comment',
description: 'Detect if a JS multi-line comment is left open',
thrower: function checkForComments({ content }) {
const openingComments = content.match(/\/\*/gi);
const closingComments = content.match(/\*\//gi);
thrower: function checkForComments({ contents }) {
const openingComments = contents.match(/\/\*/gi);
const closingComments = contents.match(/\*\//gi);
if (
openingComments &&
(!closingComments || openingComments.length > closingComments.length)
@@ -20,8 +20,8 @@ const throwForJsHtml = {
name: 'nested-jQuery',
description: 'Nested dollar sign calls breaks browsers',
detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi,
thrower: function checkForNestedJquery({ content }) {
if (content.match(this.detectUnsafeJQ)) {
thrower: function checkForNestedJquery({ contents }) {
if (contents.match(this.detectUnsafeJQ)) {
throw new Error('Unsafe $($)');
}
}
@@ -29,10 +29,10 @@ const throwForJsHtml = {
name: 'unfinished-function',
description: 'lonely function keywords breaks browsers',
detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi,
thower: function checkForUnfinishedFunction({ content: code }) {
thrower: function checkForUnfinishedFunction({ contents }) {
if (
code.match(/function/g) &&
!code.match(this.detectFunctionCall)
contents.match(/function/g) &&
!contents.match(this.detectFunctionCall)
) {
throw new Error(
'SyntaxError: Unsafe or unfinished function declaration'
@@ -43,8 +43,8 @@ const throwForJsHtml = {
name: 'unsafe console call',
description: 'console call stops tests scripts from running',
detectUnsafeConsoleCall: /if\s\(null\)\sconsole\.log\(1\);/gi,
thrower: function checkForUnsafeConsole({ content }) {
if (content.match(this.detectUnsafeConsoleCall)) {
thrower: function checkForUnsafeConsole({ contents }) {
if (contents.match(this.detectUnsafeConsoleCall)) {
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;
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.from(throwForJsHtml.throwers)
.flatMap(({ thrower }) => {
.flatMap(context => {
try {
let finalObs;
const maybeObservableOrPromise = thrower(file);
const maybeObservableOrPromise = context.thrower(file);
if (helpers.isPromise(maybeObservableOrPromise)) {
finalObs = Observable.fromPromise(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 hardGoToSaga from './hard-go-to-saga';
import windowSaga from './window-saga';
import executeChallengeSaga from './execute-challenge-saga';
import frameSaga from './frame-saga';
export default [
errSaga,
titleSaga,
localStorageSaga,
hardGoToSaga,
windowSaga
windowSaga,
executeChallengeSaga,
frameSaga
];

View File

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

View File

@@ -36,6 +36,7 @@ export class Editor extends PureComponent {
}
static displayName = 'Editor';
static propTypes = {
executeChallenge: PropTypes.func,
height: PropTypes.number,
content: PropTypes.string,
mode: PropTypes.string,
@@ -47,6 +48,40 @@ export class Editor extends PureComponent {
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() {
const { updateFile = (() => {}) } = this.props;
this._subscription = this._editorContent$
@@ -72,7 +107,7 @@ export class Editor extends PureComponent {
}
render() {
const { content, height, mode } = this.props;
const { executeChallenge, content, height, mode } = this.props;
const style = {};
if (height) {
style.height = height + 'px';
@@ -84,7 +119,7 @@ export class Editor extends PureComponent {
<NoSSR>
<Codemirror
onChange={ this.handleChange }
options={{ ...options, mode }}
options={ this.createOptions({ executeChallenge, mode, options }) }
value={ content } />
</NoSSR>
</div>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import PureComponent from 'react-pure-render/component';
const mainId = 'fcc-main-frame';
export default class extends PureComponent {
static displayName = 'Preview';
@@ -14,7 +15,7 @@ export default class extends PureComponent {
</div>
<iframe
className='iphone iframe-scroll'
id='preview' />
id={ mainId } />
<div className='spacer' />
</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 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 propTypes = {
executeChallenge: PropTypes.func
};
render() {
const { executeChallenge } = this.props;
return (
<div>
<Button
block={ true }
bsStyle='primary'
className='btn-big'>
className='btn-big'
onClick={ executeChallenge }>
Run tests (ctrl + enter)
</Button>
<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,
(content, file) => updateContents(content, file)
);
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 { BONFIRE, HTML, JS } from '../../../utils/challengeTypes';
import { arrayToString, buildSeed, createTests, getPath } from '../utils';
import {
arrayToString,
buildSeed,
createTests,
getPreFile,
getFileKey
} from '../utils';
const initialState = {
challenge: '',
@@ -24,7 +30,7 @@ const mainReducer = handleActions(
...state,
refresh: true,
challenge: challenge.dashedName,
path: getPath(challenge),
key: getFileKey(challenge),
tests: createTests(challenge)
}),
@@ -57,12 +63,12 @@ const filesReducer = handleActions(
{
[types.updateFile]: (state, { payload: file }) => ({
...state,
[file.path]: file
[file.key]: file
}),
[types.updateFiles]: (state, { payload: files }) => {
return files
.reduce((files, file) => {
files[file.path] = file;
files[file.key] = file;
return files;
}, { ...state });
},
@@ -77,11 +83,11 @@ const filesReducer = handleActions(
) {
return {};
}
const path = getPath(challenge);
const preFile = getPreFile(challenge);
return {
...state,
[path]: createPoly({
path,
[preFile.key]: createPoly({
...preFile,
contents: buildSeed(challenge),
head: arrayToString(challenge.head),
tail: arrayToString(challenge.tail)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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