Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 60 additions & 13 deletions loaders/examples.loader.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var marked = require('marked');
var config = require('../src/utils/config');

var defaultRenderer = marked.Renderer.prototype;
var evalPlaceholder = '<%{#eval#}%>';
var codePlaceholder = '<%{#code#}%>';

var requireAnythingRegex = /require\s*\(([^)]+)\)/g;
var simpleStringRegex = /^"([^"]+)"$|^'([^']+)'$/;

function readExamples(markdown) {
var codePlaceholder = '<%{#}%>';
var codeChunks = [];

var renderer = new marked.Renderer();
Expand All @@ -29,22 +30,68 @@ function readExamples(markdown) {
}
var code = codeChunks.shift();
if (code) {
chunks.push({type: 'code', content: code});
chunks.push({type: 'code', content: code, evalInContext: evalPlaceholder});
}
});

return chunks;
}

module.exports = function (source, map) {
// Returns a list of all strings used in require(...) calls in the given source code.
// If there is any other expression inside the require call, it throws an error.
function findRequires(codeString) {
var requires = {};
codeString.replace(requireAnythingRegex, function(requireExprMatch, requiredExpr) {
var requireStrMatch = simpleStringRegex.exec(requiredExpr.trim());
if (!requireStrMatch) {
throw new Error('Requires using expressions are not supported in examples. (Used: ' + requireExprMatch + ')');
}
var requiredString = requireStrMatch[1] ? requireStrMatch[1] : requireStrMatch[2];
requires[requiredString] = true;
});
return Object.keys(requires);
}

function examplesLoader(source, map) {
this.cacheable && this.cacheable();

var examples = readExamples(source);

// We're analysing the examples' source code to figure out the requires. We do it manually with
// regexes, because webpack unfortunately doesn't expose its smart logic for rewriting requires
// (https://webpack.github.io/docs/context.html). Note that we can't just use require(...)
// directly in runtime, because webpack changes its name to __webpack__require__ or sth.
// Related PR: https://github.com/sapegin/react-styleguidist/pull/25
var codeFromAllExamples = _.map(_.filter(examples, {type: 'code'}), 'content').join('\n');
var requiresFromExamples = findRequires(codeFromAllExamples);

return [
'if (module.hot) {',
' module.hot.accept([]);',
'}',
'module.exports = ' + JSON.stringify(examples)
].join('\n');
};
'if (module.hot) {',
' module.hot.accept([]);',
'}',
'var requireMap = {',
requiresFromExamples.map(function(requireRequest) {
return ' ' + JSON.stringify(requireRequest) + ': require(' + JSON.stringify(requireRequest) + ')';
}).join(',\n'),
'};',
'function requireInRuntime(path) {',
' if (!requireMap.hasOwnProperty(path)) {',
' throw new Error("Sorry, changing requires in runtime is currently not supported.")',
' }',
' return requireMap[path];',
'}',
'module.exports = ' + JSON.stringify(examples).replace(
new RegExp(_.escapeRegExp('"' + evalPlaceholder + '"'), 'g'),
'function(code) {var require=requireInRuntime; return eval(code);}'
) + ';',
].join('\n');
}

_.assign(examplesLoader, {
requireAnythingRegex: requireAnythingRegex,
simpleStringRegex: simpleStringRegex,
readExamples: readExamples,
findRequires: findRequires,
});

module.exports = examplesLoader;
5 changes: 3 additions & 2 deletions src/components/Playground/Playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import s from './Playground.css';
export default class Playground extends Component {
static propTypes = {
highlightTheme: PropTypes.string.isRequired,
code: PropTypes.string.isRequired
code: PropTypes.string.isRequired,
evalInContext: PropTypes.func.isRequired,
}

constructor(props) {
Expand Down Expand Up @@ -39,7 +40,7 @@ export default class Playground extends Component {
return (
<div className={s.root}>
<div className={s.preview}>
<Preview code={code}/>
<Preview code={code} evalInContext={this.props.evalInContext}/>
</div>
<div className={s.editor}>
<Editor code={code} highlightTheme={highlightTheme} onChange={this.handleChange}/>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Preview/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import s from './Preview.css';

export default class Preview extends Component {
static propTypes = {
code: PropTypes.string.isRequired
code: PropTypes.string.isRequired,
evalInContext: PropTypes.func.isRequired,
}

constructor() {
Expand Down Expand Up @@ -49,7 +50,7 @@ export default class Preview extends Component {

try {
let compiledCode = this.compileCode(code);
let component = eval(compiledCode); /* eslint no-eval:0 */
let component = this.props.evalInContext(compiledCode);
let wrappedComponent = (
<Wrapper>
{component}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ReactComponent/ReactComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class ReactComponent extends Component {
switch (example.type) {
case 'code':
return (
<Playground code={example.content} highlightTheme={highlightTheme} key={index}/>
<Playground code={example.content} evalInContext={example.evalInContext} highlightTheme={highlightTheme} key={index} />
);
case 'html':
return (
Expand Down
92 changes: 92 additions & 0 deletions test/examples.loader.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { expect } from 'chai';

import examplesLoader from '../loaders/examples.loader';

/* eslint max-nested-callbacks: [1, 5] */

describe('examples loader', () => {

describe('requireAnythingRegex', () => {

let regex;
beforeEach(() => {
expect(examplesLoader.requireAnythingRegex).to.be.an.instanceof(RegExp);
// we make a version without the /g flag
regex = new RegExp(examplesLoader.requireAnythingRegex, '');
});

it('should match require invocations', () => {
expect(`require("foo")`).to.match(regex);
expect(`require ( "foo" )`).to.match(regex);
expect(`require('foo')`).to.match(regex);
expect(`require(foo)`).to.match(regex);
expect(`require("f" + "o" + "o")`).to.match(regex);
expect(`require("f" + ("o" + "o"))`).to.match(regex);
expect(`function f() { require("foo"); }`).to.match(regex);
});

it('should not match other occurences of require', () => {
expect(`"required field"`).not.to.match(regex);
expect(`var f = require;`).not.to.match(regex);
expect(`require.call(module, "foo")`).not.to.match(regex);
});

it('should match many requires in the same line correctly', () => {
var replaced = `require('foo');require('bar')`.replace(examplesLoader.requireAnythingRegex, 'x');
expect(replaced).to.equal('x;x');
});
});

describe('simpleStringRegex', () => {
it('should match simple strings and nothing else', () => {
let regex = examplesLoader.simpleStringRegex;

expect(`"foo"`).to.match(regex);
expect(`'foo'`).to.match(regex);
expect(`"fo'o"`).to.match(regex);
expect(`'fo"o'`).to.match(regex);
expect(`'.,:;!§$&/()=@^12345'`).to.match(regex);

expect(`foo`).not.to.match(regex);
expect(`'foo"`).not.to.match(regex);
expect(`"foo'`).not.to.match(regex);

// these 2 are actually valid in JS, but don't work with this regex.
// But you shouldn't be using these in your requires anyway.
expect(`"fo\\"o"`).not.to.match(regex);
expect(`'fo\\'o'`).not.to.match(regex);

expect(`"foo" + "bar"`).not.to.match(regex);
});
});

describe('findRequires', () => {
it('should find calls to require in code', () => {
let findRequires = examplesLoader.findRequires;
expect(findRequires(`require('foo')`)).to.deep.equal(['foo']);
expect(findRequires(`require('./foo')`)).to.deep.equal(['./foo']);
expect(findRequires(`require('foo');require('bar')`)).to.deep.equal(['foo', 'bar']);
expect(() => findRequires(`require('foo' + 'bar')`)).to.throw(Error);
});
});

describe('readExamples', () => {
it('should separate code and html chunks', () => {
let examplesMarkdown = '# header\n\n <div />\n\ntext';
let examples = examplesLoader.readExamples(examplesMarkdown);
expect(examples).to.have.length(3);
expect(examples[0].type).to.equal('html');
expect(examples[1].type).to.equal('code');
expect(examples[2].type).to.equal('html');
});
});

describe('loader', () => {
it('should return valid, parsable js', () => {
let exampleMarkdown = '# header\n\n <div />\n\ntext';
let output = examplesLoader.call({}, exampleMarkdown);
expect(() => new Function(output)).not.to.throw(SyntaxError); // eslint-disable-line no-new-func
});
});

});