Skip to content
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 sharing of the REPL #25

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CodeMirror.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require('./paraiso-light.css');
require('codemirror/mode/xml/xml');
require('codemirror/mode/javascript/javascript');
require('codemirror/keymap/sublime');
require('codemirror/addon/display/placeholder');

const DEFAULT_CODE_MIRROR_OPTIONS = {
autoCloseBrackets: true,
Expand Down
43 changes: 23 additions & 20 deletions src/Repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@ import React, { Component } from 'react';
import { debounce, cloneDeep } from 'lodash-es';

import CodeMirrorPanel from './CodeMirrorPanel';
import { getCodeSizeInBytes } from './lib/helpers';
import { getCodeSizeInBytes, loadState, saveState } from './lib/helpers';
import terserOptions, { evalOptions } from './lib/terser-options';

import styles from './Repl.module.css';

const DEBOUNCE_DELAY = 500;

const defaultState = {
optionsCode: terserOptions,
code: '',
minified: '',
terserOptions: evalOptions(),
rawSize: 0,
minifiedSize: 0,
};

class Repl extends Component {
state = {
optionsCode: terserOptions,
code: '// write or paste code here\n\n',
minified: "// terser's output will be shown here",
terserOptions: evalOptions(),
rawSize: 0,
minifiedSize: 0
};
state = loadState() || defaultState;

options = {
_minifyId = 0;
_options = {
lineWrapping: true,
fileSize: true
};
Expand All @@ -37,26 +40,26 @@ class Repl extends Component {
options={{ lineWrapping: true }}
theme="paraiso-light"
errorMessage={this.state.optionsErrorMessage}
placeholder="Edit terser config here"
placeholder="// Edit terser config here"
/>
<CodeMirrorPanel
className={styles.codeMirrorPanelInput}
code={this.state.code}
onChange={this._updateCode}
options={this.options}
options={this._options}
fileSize={this.state.rawSize}
theme="paraiso-light"
errorMessage={this.state.errorMessage}
placeholder="Write or paste code here"
placeholder="// Write or paste code here"
/>
</div>
<CodeMirrorPanel
className={styles.codeMirrorPanel}
code={this.state.minified}
options={this.options}
options={this._options}
fileSize={this.state.minifiedSize}
theme="paraiso-dark"
placeholder="Terser output will be shown here"
placeholder="// Terser output will be shown here"
/>
</div>
</div>
Expand Down Expand Up @@ -84,21 +87,20 @@ class Repl extends Component {
this.setState({ optionsErrorMessage: e.message });
}

this._minify(this.state.code);
this._minifyToState(this.state.code);
};

_minifyToState = debounce(
code => this._minify(code, this._persistState),
DEBOUNCE_DELAY
);
_minifyToState = debounce(code => this._minify(code), DEBOUNCE_DELAY);

_minify = async (code, setStateCallback) => {
// we need to clone this because terser mutates the options object :(
const terserOpts = cloneDeep(this.state.terserOptions);

const minifyId = ++this._minifyId;
// TODO: put this in a worker to avoid blocking the UI on heavy content
try {
const result = await this.props.terser.minify(code, terserOpts);
if (this._minifyId !== minifyId) return;

if (result.error) {
this.setState({ errorMessage: result.error.message });
Expand All @@ -108,6 +110,7 @@ class Repl extends Component {
minifiedSize: getCodeSizeInBytes(result.code),
errorMessage: null
});
saveState(this.state);
}
} catch (e) {
this.setState({ errorMessage: e.message });
Expand Down
31 changes: 31 additions & 0 deletions src/lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
export const getCodeSizeInBytes = code => {
return new Blob([code], { type: 'text/plain' }).size;
};

const base64UrlDecodeChars = {'-': '+', '_': '/', '.': '='};
const base64UrlDecode = input => {
try {
return atob(input.replace(/[-_.]/g, c => base64UrlDecodeChars[c]));
} catch {
return null;
}
}

const base64UrlEncodeChars = {'+': '-', '/': '_', '=': '.'};
const base64UrlEncode = input => {
try {
return btoa(input).replace(/[+/=]/g, c => base64UrlEncodeChars[c]);
} catch {
return null;
}
}
Comment on lines +5 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart!


export const saveState = state => {
const base64 = base64UrlEncode(JSON.stringify(state));
window.history.replaceState(state, '', `#${base64}`);
};

export const loadState = () => {
try {
const base64 = window.location.hash.slice(1);
return JSON.parse(base64UrlDecode(base64));
} catch {}
return null;
}
12 changes: 10 additions & 2 deletions src/lib/terser-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,15 @@ const options = `// edit terser options
rename: {},
}`;

/* eslint-disable-next-line no-eval */
export const evalOptions = (opts) => eval(`(${opts||options})`)
export const evalOptions = (opts) => {
opts = opts || options;
// Strip line comments
opts = opts.replace(/\/\/.*/g, '');
// Trim trailing commas
opts = opts.replace(/,\s*([\]}])/g, '$1');
// Quote property names
opts = opts.replace(/^\s*(\w+):/gm, '"$1":');
return JSON.parse(opts);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why this is something we need to do (avoiding a self xss and whatnot), however there are some Terser options that are regexps. For instance you can use /^_/ to select properties started with _ to mangle :/

There are a few options that take functions too (which would be a nightmare) so I'm not mentioning that part.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's possible to avoid this by disabling sharing for options that include RegExps and functions. Then you can do eval freely, and only do JSON.parse in loadState


export default options;