From 861f89683bc6110da8369d9a825a01c9d16cd994 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 6 May 2016 13:20:18 -0700 Subject: [PATCH] Initial work on new framework --- client/new-framework/add-loop-protect.js | 21 +++++ .../new-framework/execute-challenge-saga.js | 13 +++ client/new-framework/polyvinyl.js | 92 +++++++++++++++++++ client/new-framework/redux/types.js | 0 client/new-framework/throw-unsafe-code.js | 89 ++++++++++++++++++ package.json | 2 + webpack.config.js | 6 +- 7 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 client/new-framework/add-loop-protect.js create mode 100644 client/new-framework/execute-challenge-saga.js create mode 100644 client/new-framework/polyvinyl.js create mode 100644 client/new-framework/redux/types.js create mode 100644 client/new-framework/throw-unsafe-code.js diff --git a/client/new-framework/add-loop-protect.js b/client/new-framework/add-loop-protect.js new file mode 100644 index 0000000000..05d986e1a6 --- /dev/null +++ b/client/new-framework/add-loop-protect.js @@ -0,0 +1,21 @@ +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; + })); +} diff --git a/client/new-framework/execute-challenge-saga.js b/client/new-framework/execute-challenge-saga.js new file mode 100644 index 0000000000..7044642e0e --- /dev/null +++ b/client/new-framework/execute-challenge-saga.js @@ -0,0 +1,13 @@ +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; + } + }) +} diff --git a/client/new-framework/polyvinyl.js b/client/new-framework/polyvinyl.js new file mode 100644 index 0000000000..4abbd304d4 --- /dev/null +++ b/client/new-framework/polyvinyl.js @@ -0,0 +1,92 @@ +// originally base off of https://github.com/gulpjs/vinyl +import path from 'path'; +import replaceExt from 'replace-ext'; + +export default class File { + constructor({ + path, + history = [], + base, + contents = '' + } = {}) { + // Record path change + this.history = path ? [path] : history; + this.base = base || this.cwd; + this.contents = contents; + this._isPolyVinyl = true; + this.error = null; + } + + static isPolyVinyl = function(file) { + return file && file._isPolyVinyl === true || false; + }; + + isEmpty() { + return !this._contents; + } + + get contents() { + return this._contents; + } + + set contents(val) { + if (typeof val !== 'string') { + throw new TypeError('File.contents can only a String'); + } + this._contents = val; + } + + get basename() { + if (!this.path) { + throw new Error('No path specified! Can not get basename.'); + } + return path.basename(this.path); + } + + set basename(basename) { + if (!this.path) { + throw new Error('No path specified! Can not set basename.'); + } + this.path = path.join(path.dirname(this.path), basename); + } + + get extname() { + if (!this.path) { + throw new Error('No path specified! Can not get extname.'); + } + return path.extname(this.path); + } + + set extname(extname) { + if (!this.path) { + throw new Error('No path specified! Can not set extname.'); + } + this.path = replaceExt(this.path, extname); + } + + get path() { + return this.history[this.history.length - 1]; + } + + set path(path) { + if (typeof path !== 'string') { + throw new TypeError('path should be string'); + } + + // Record history only when path changed + if (path && path !== this.path) { + this.history.push(path); + } + } + + get error() { + return this._error; + } + + set error(err) { + if (typeof err !== 'object') { + throw new TypeError('error must be an object or null'); + } + this.error = err; + } +} diff --git a/client/new-framework/redux/types.js b/client/new-framework/redux/types.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/client/new-framework/throw-unsafe-code.js b/client/new-framework/throw-unsafe-code.js new file mode 100644 index 0000000000..567fa62152 --- /dev/null +++ b/client/new-framework/throw-unsafe-code.js @@ -0,0 +1,89 @@ +import { helpers, Observable } from 'rx'; + +const throwForJsHtml = { + extname: /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); + if ( + openingComments && + (!closingComments || openingComments.length > closingComments.length) + ) { + throw new Error('SyntaxError: Unfinished multi-line comment'); + } + } + }, { + name: 'nested-jQuery', + description: 'Nested dollar sign calls breaks browsers', + detectUnsafeJQ: /\$\s*?\(\s*?\$\s*?\)/gi, + thrower: function checkForNestedJquery({ content }) { + if (content.match(this.detectUnsafeJQ)) { + throw new Error('Unsafe $($)'); + } + } + }, { + name: 'unfinished-function', + description: 'lonely function keywords breaks browsers', + detectFunctionCall: /function\s*?\(|function\s+\w+\s*?\(/gi, + thower: function checkForUnfinishedFunction({ content: code }) { + if ( + code.match(/function/g) && + !code.match(this.detectFunctionCall) + ) { + throw new Error( + 'SyntaxError: Unsafe or unfinished function declaration' + ); + } + } + }, { + 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)) { + throw new Error('Invalid if (null) console.log(1); detected'); + } + } + } + ] +}; + +export default function pretester() { + const source = this; + return source.map(file$ => file$.flatMap(file => { + if (!throwForJsHtml.extname.test(file.extname)) { + return Observable.just(file); + } + return Observable.from(throwForJsHtml.throwers) + .flatMap(({ thrower }) => { + try { + let finalObs; + const maybeObservableOrPromise = thrower(file); + if (helpers.isPromise(maybeObservableOrPromise)) { + finalObs = Observable.fromPromise(maybeObservableOrPromise); + } else if (Observable.isObservable(maybeObservableOrPromise)) { + finalObs = maybeObservableOrPromise; + } else { + finalObs = Observable.just(maybeObservableOrPromise); + } + return finalObs; + } catch (err) { + return Observable.throw(err); + } + }) + // if none of the throwers throw, wait for last one + .last({ defaultValue: null }) + // then map to the original file + .map(file) + // if err add it to the file + // and return file + .catch(err => { + file.error = err; + return Observable.just(file); + }); + })); +} diff --git a/package.json b/package.json index 71b2cb10ca..132f6de1d4 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "pmx": "~0.6.2", "react": "^15.0.2", "react-bootstrap": "~0.29.4", + "react-codemirror": "^0.2.6", "react-css-transition-replace": "^1.2.0-beta", "react-dom": "^15.0.2", "react-fontawesome": "^0.3.3", @@ -97,6 +98,7 @@ "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", diff --git a/webpack.config.js b/webpack.config.js index bde282836b..3228a5e950 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -39,16 +39,16 @@ module.exports = { ] }, externals: { - 'codemirror': 'CodeMirror' + codemirror: 'CodeMirror' }, plugins: [ new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(true), new webpack.DefinePlugin({ 'process.env': { - 'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production') + NODE_ENV: JSON.stringify(__DEV__ ? 'development' : 'production') }, - '__DEVTOOLS__': !__DEV__ + __DEVTOOLS__: !__DEV__ }) ] };