Skip to content

Commit 706249d

Browse files
committed
Merge pull request #25 from lovelybooks/require-from-examples
Added support for using require(...) in code examples.
2 parents eaa14bb + cd74707 commit 706249d

File tree

5 files changed

+159
-18
lines changed

5 files changed

+159
-18
lines changed

loaders/examples.loader.js

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
var fs = require('fs');
2-
var path = require('path');
1+
var _ = require('lodash');
32
var marked = require('marked');
4-
var config = require('../src/utils/config');
53

6-
var defaultRenderer = marked.Renderer.prototype;
4+
var evalPlaceholder = '<%{#eval#}%>';
5+
var codePlaceholder = '<%{#code#}%>';
6+
7+
var requireAnythingRegex = /require\s*\(([^)]+)\)/g;
8+
var simpleStringRegex = /^"([^"]+)"$|^'([^']+)'$/;
79

810
function readExamples(markdown) {
9-
var codePlaceholder = '<%{#}%>';
1011
var codeChunks = [];
1112

1213
var renderer = new marked.Renderer();
@@ -29,22 +30,68 @@ function readExamples(markdown) {
2930
}
3031
var code = codeChunks.shift();
3132
if (code) {
32-
chunks.push({type: 'code', content: code});
33+
chunks.push({type: 'code', content: code, evalInContext: evalPlaceholder});
3334
}
3435
});
3536

3637
return chunks;
3738
}
3839

39-
module.exports = function (source, map) {
40+
// Returns a list of all strings used in require(...) calls in the given source code.
41+
// If there is any other expression inside the require call, it throws an error.
42+
function findRequires(codeString) {
43+
var requires = {};
44+
codeString.replace(requireAnythingRegex, function(requireExprMatch, requiredExpr) {
45+
var requireStrMatch = simpleStringRegex.exec(requiredExpr.trim());
46+
if (!requireStrMatch) {
47+
throw new Error('Requires using expressions are not supported in examples. (Used: ' + requireExprMatch + ')');
48+
}
49+
var requiredString = requireStrMatch[1] ? requireStrMatch[1] : requireStrMatch[2];
50+
requires[requiredString] = true;
51+
});
52+
return Object.keys(requires);
53+
}
54+
55+
function examplesLoader(source, map) {
4056
this.cacheable && this.cacheable();
4157

4258
var examples = readExamples(source);
4359

60+
// We're analysing the examples' source code to figure out the requires. We do it manually with
61+
// regexes, because webpack unfortunately doesn't expose its smart logic for rewriting requires
62+
// (https://webpack.github.io/docs/context.html). Note that we can't just use require(...)
63+
// directly in runtime, because webpack changes its name to __webpack__require__ or sth.
64+
// Related PR: https://github.com/sapegin/react-styleguidist/pull/25
65+
var codeFromAllExamples = _.map(_.filter(examples, {type: 'code'}), 'content').join('\n');
66+
var requiresFromExamples = findRequires(codeFromAllExamples);
67+
4468
return [
45-
'if (module.hot) {',
46-
' module.hot.accept([]);',
47-
'}',
48-
'module.exports = ' + JSON.stringify(examples)
49-
].join('\n');
50-
};
69+
'if (module.hot) {',
70+
' module.hot.accept([]);',
71+
'}',
72+
'var requireMap = {',
73+
requiresFromExamples.map(function(requireRequest) {
74+
return ' ' + JSON.stringify(requireRequest) + ': require(' + JSON.stringify(requireRequest) + ')';
75+
}).join(',\n'),
76+
'};',
77+
'function requireInRuntime(path) {',
78+
' if (!requireMap.hasOwnProperty(path)) {',
79+
' throw new Error("Sorry, changing requires in runtime is currently not supported.")',
80+
' }',
81+
' return requireMap[path];',
82+
'}',
83+
'module.exports = ' + JSON.stringify(examples).replace(
84+
new RegExp(_.escapeRegExp('"' + evalPlaceholder + '"'), 'g'),
85+
'function(code) {var require=requireInRuntime; return eval(code);}'
86+
) + ';',
87+
].join('\n');
88+
}
89+
90+
_.assign(examplesLoader, {
91+
requireAnythingRegex: requireAnythingRegex,
92+
simpleStringRegex: simpleStringRegex,
93+
readExamples: readExamples,
94+
findRequires: findRequires,
95+
});
96+
97+
module.exports = examplesLoader;

src/components/Playground/Playground.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import s from './Playground.css';
77
export default class Playground extends Component {
88
static propTypes = {
99
highlightTheme: PropTypes.string.isRequired,
10-
code: PropTypes.string.isRequired
10+
code: PropTypes.string.isRequired,
11+
evalInContext: PropTypes.func.isRequired,
1112
}
1213

1314
constructor(props) {
@@ -39,7 +40,7 @@ export default class Playground extends Component {
3940
return (
4041
<div className={s.root}>
4142
<div className={s.preview}>
42-
<Preview code={code}/>
43+
<Preview code={code} evalInContext={this.props.evalInContext}/>
4344
</div>
4445
<div className={s.editor}>
4546
<Editor code={code} highlightTheme={highlightTheme} onChange={this.handleChange}/>

src/components/Preview/Preview.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import s from './Preview.css';
99

1010
export default class Preview extends Component {
1111
static propTypes = {
12-
code: PropTypes.string.isRequired
12+
code: PropTypes.string.isRequired,
13+
evalInContext: PropTypes.func.isRequired,
1314
}
1415

1516
constructor() {
@@ -49,7 +50,7 @@ export default class Preview extends Component {
4950

5051
try {
5152
let compiledCode = this.compileCode(code);
52-
let component = eval(compiledCode); /* eslint no-eval:0 */
53+
let component = this.props.evalInContext(compiledCode);
5354
let wrappedComponent = (
5455
<Wrapper>
5556
{component}

src/components/ReactComponent/ReactComponent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default class ReactComponent extends Component {
3131
switch (example.type) {
3232
case 'code':
3333
return (
34-
<Playground code={example.content} highlightTheme={highlightTheme} key={index}/>
34+
<Playground code={example.content} evalInContext={example.evalInContext} highlightTheme={highlightTheme} key={index} />
3535
);
3636
case 'html':
3737
return (

test/examples.loader.spec.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { expect } from 'chai';
2+
3+
import examplesLoader from '../loaders/examples.loader';
4+
5+
/* eslint max-nested-callbacks: [1, 5] */
6+
7+
describe('examples loader', () => {
8+
9+
describe('requireAnythingRegex', () => {
10+
11+
let regex;
12+
beforeEach(() => {
13+
expect(examplesLoader.requireAnythingRegex).to.be.an.instanceof(RegExp);
14+
// we make a version without the /g flag
15+
regex = new RegExp(examplesLoader.requireAnythingRegex, '');
16+
});
17+
18+
it('should match require invocations', () => {
19+
expect(`require("foo")`).to.match(regex);
20+
expect(`require ( "foo" )`).to.match(regex);
21+
expect(`require('foo')`).to.match(regex);
22+
expect(`require(foo)`).to.match(regex);
23+
expect(`require("f" + "o" + "o")`).to.match(regex);
24+
expect(`require("f" + ("o" + "o"))`).to.match(regex);
25+
expect(`function f() { require("foo"); }`).to.match(regex);
26+
});
27+
28+
it('should not match other occurences of require', () => {
29+
expect(`"required field"`).not.to.match(regex);
30+
expect(`var f = require;`).not.to.match(regex);
31+
expect(`require.call(module, "foo")`).not.to.match(regex);
32+
});
33+
34+
it('should match many requires in the same line correctly', () => {
35+
var replaced = `require('foo');require('bar')`.replace(examplesLoader.requireAnythingRegex, 'x');
36+
expect(replaced).to.equal('x;x');
37+
});
38+
});
39+
40+
describe('simpleStringRegex', () => {
41+
it('should match simple strings and nothing else', () => {
42+
let regex = examplesLoader.simpleStringRegex;
43+
44+
expect(`"foo"`).to.match(regex);
45+
expect(`'foo'`).to.match(regex);
46+
expect(`"fo'o"`).to.match(regex);
47+
expect(`'fo"o'`).to.match(regex);
48+
expect(`'.,:;!§$&/()=@^12345'`).to.match(regex);
49+
50+
expect(`foo`).not.to.match(regex);
51+
expect(`'foo"`).not.to.match(regex);
52+
expect(`"foo'`).not.to.match(regex);
53+
54+
// these 2 are actually valid in JS, but don't work with this regex.
55+
// But you shouldn't be using these in your requires anyway.
56+
expect(`"fo\\"o"`).not.to.match(regex);
57+
expect(`'fo\\'o'`).not.to.match(regex);
58+
59+
expect(`"foo" + "bar"`).not.to.match(regex);
60+
});
61+
});
62+
63+
describe('findRequires', () => {
64+
it('should find calls to require in code', () => {
65+
let findRequires = examplesLoader.findRequires;
66+
expect(findRequires(`require('foo')`)).to.deep.equal(['foo']);
67+
expect(findRequires(`require('./foo')`)).to.deep.equal(['./foo']);
68+
expect(findRequires(`require('foo');require('bar')`)).to.deep.equal(['foo', 'bar']);
69+
expect(() => findRequires(`require('foo' + 'bar')`)).to.throw(Error);
70+
});
71+
});
72+
73+
describe('readExamples', () => {
74+
it('should separate code and html chunks', () => {
75+
let examplesMarkdown = '# header\n\n <div />\n\ntext';
76+
let examples = examplesLoader.readExamples(examplesMarkdown);
77+
expect(examples).to.have.length(3);
78+
expect(examples[0].type).to.equal('html');
79+
expect(examples[1].type).to.equal('code');
80+
expect(examples[2].type).to.equal('html');
81+
});
82+
});
83+
84+
describe('loader', () => {
85+
it('should return valid, parsable js', () => {
86+
let exampleMarkdown = '# header\n\n <div />\n\ntext';
87+
let output = examplesLoader.call({}, exampleMarkdown);
88+
expect(() => new Function(output)).not.to.throw(SyntaxError); // eslint-disable-line no-new-func
89+
});
90+
});
91+
92+
});

0 commit comments

Comments
 (0)