Skip to content

Commit 274598d

Browse files
authored
Support for cyclic dependencies (codesandbox#42)
* Support for cyclic dependencies * Tests
1 parent ec5b4d1 commit 274598d

File tree

4 files changed

+146
-11
lines changed

4 files changed

+146
-11
lines changed

src/sandbox/eval/index.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,65 @@ function doEval(
1414
directories: Array<Directory>,
1515
externals: Object,
1616
depth: ?number,
17+
parentModule: ?Module,
1718
) {
1819
const html = /\.html$/;
1920
const css = /\.css$/;
2021
const json = /\.json$/;
2122
const js = /\.js$/;
2223

2324
if (html.test(mainModule.title)) {
24-
return evalRaw(mainModule, modules, directories, externals, depth);
25+
return evalRaw(
26+
mainModule,
27+
modules,
28+
directories,
29+
externals,
30+
depth,
31+
parentModule,
32+
);
2533
}
2634

2735
if (css.test(mainModule.title)) {
28-
return evalCSS(mainModule, modules, directories, externals, depth);
36+
return evalCSS(
37+
mainModule,
38+
modules,
39+
directories,
40+
externals,
41+
depth,
42+
parentModule,
43+
);
2944
}
3045

3146
if (json.test(mainModule.title) || mainModule.title === '.babelrc') {
32-
return evalJson(mainModule, modules, directories, externals, depth);
47+
return evalJson(
48+
mainModule,
49+
modules,
50+
directories,
51+
externals,
52+
depth,
53+
parentModule,
54+
);
3355
}
3456

3557
if (js.test(mainModule.title)) {
36-
return evalJS(mainModule, modules, directories, externals, depth);
58+
return evalJS(
59+
mainModule,
60+
modules,
61+
directories,
62+
externals,
63+
depth,
64+
parentModule,
65+
);
3766
}
3867

39-
return evalRaw(mainModule, modules, directories, externals, depth);
68+
return evalRaw(
69+
mainModule,
70+
modules,
71+
directories,
72+
externals,
73+
depth,
74+
parentModule,
75+
);
4076
}
4177

4278
export function deleteCache(module: Module) {
@@ -49,14 +85,22 @@ const evalModule = (
4985
directories: Array<Directory>,
5086
externals: Object,
5187
depth: number = 0,
88+
parentModule: Array<Module> = [],
5289
) => {
5390
if (depth > MAX_DEPTH) {
5491
throw new Error(
5592
`Exceeded the maximum require depth of ${MAX_DEPTH}, there are probably two files depending on eachother.`,
5693
);
5794
}
5895
try {
59-
return doEval(mainModule, modules, directories, externals, depth);
96+
return doEval(
97+
mainModule,
98+
modules,
99+
directories,
100+
externals,
101+
depth,
102+
parentModule,
103+
);
60104
} catch (e) {
61105
e.module = e.module || mainModule;
62106
throw e;

src/sandbox/eval/index.test.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import evaller from './';
2+
import { clearCache } from './js';
23

34
describe('eval', () => {
45
// just evaluate if the right evallers are called
56
describe('js', () => {
7+
beforeEach(() => {
8+
clearCache();
9+
});
10+
611
test('default es exports', () => {
712
const mainModule = {
813
title: 'test.js',
@@ -60,7 +65,66 @@ describe('eval', () => {
6065
};
6166

6267
expect(evaller(mainModule, [mainModule, secondModule], [])).toEqual({
63-
default: 3,
68+
default: { default: 3 },
69+
});
70+
});
71+
72+
describe('cyclic dependencies', () => {
73+
it('returns an object as cyclic dependency', () => {
74+
const moduleA = {
75+
title: 'a.js',
76+
shortid: '1',
77+
code: `
78+
import b from './b';
79+
export default b;
80+
`,
81+
};
82+
83+
const moduleB = {
84+
title: 'b.js',
85+
shortid: '2',
86+
code: `
87+
import a from './a';
88+
export default a;
89+
`,
90+
};
91+
92+
expect(evaller(moduleA, [moduleA, moduleB], [])).toEqual({
93+
default: {},
94+
});
95+
});
96+
97+
it('returns an object in deep cyclic dependency', () => {
98+
const moduleA = {
99+
title: 'a.js',
100+
shortid: '1',
101+
code: `
102+
import b from './b';
103+
export default b;
104+
`,
105+
};
106+
107+
const moduleB = {
108+
title: 'b.js',
109+
shortid: '2',
110+
code: `
111+
import c from './c';
112+
export default c;
113+
`,
114+
};
115+
116+
const moduleC = {
117+
title: 'c.js',
118+
shortid: '3',
119+
code: `
120+
import a from './a';
121+
export default a;
122+
`,
123+
};
124+
125+
expect(evaller(moduleA, [moduleA, moduleB, moduleC], [])).toEqual({
126+
default: {},
127+
});
64128
});
65129
});
66130

src/sandbox/eval/js/babel-parser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function getBabelConfig(
6262
modules: Array<Module>,
6363
directories: Array<Directory>,
6464
externals: Object,
65-
depth: number,
65+
depth: ?number,
6666
) {
6767
const babelConfigModule = modules.find(
6868
m => m.title === '.babelrc' && !m.directoryShortid,

src/sandbox/eval/js/index.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ import getBabelConfig from './babel-parser';
1010

1111
const moduleCache = new Map();
1212

13+
export function clearCache() {
14+
moduleCache.clear();
15+
}
16+
1317
/**
1418
* Deletes the cache of all modules that use module and module itself
1519
*/
1620
export function deleteCache(module: Module) {
21+
// Delete own cache first, because with cyclic dependencies we could get a
22+
// endless loop
23+
moduleCache.delete(module.id);
1724
moduleCache.forEach(value => {
1825
if (value.requires.includes(module.id)) {
1926
deleteCache(value.module);
2027
}
2128
});
22-
moduleCache.delete(module.id);
2329
}
2430

2531
const compileCode = (
@@ -45,12 +51,24 @@ function evaluate(code, require) {
4551
return Object.keys(exports).length > 0 ? exports : module.exports;
4652
}
4753

54+
/**
55+
* Transpile & execute a JS file
56+
* @param {*} mainModule The module to execute
57+
* @param {*} modules All modules in the sandbox
58+
* @param {*} directories All directories in the sandbox
59+
* @param {*} externals A list of dependency with a mapping to dependencPath -> module id
60+
* @param {*} depth The amount of requires we're deep in
61+
* @param {*} parentModules If this is a module that's required, the parents that execute it
62+
* are here (so if a requires b and b is executed, this will be [a]).
63+
* This is required for cyclic dependency checks
64+
*/
4865
export default function evaluateJS(
4966
mainModule: Module,
5067
modules: Array<Module>,
5168
directories: Array<Directory>,
5269
externals: { [path: string]: string },
53-
depth: number,
70+
depth: ?number,
71+
parentModules: Array<Module>,
5472
) {
5573
try {
5674
const requires = [];
@@ -77,9 +95,18 @@ export default function evaluateJS(
7795
// Check if this module has been evaluated before, if so return that
7896
const cache = moduleCache.get(module.id);
7997

98+
// This is a cyclic dependency, we should return an empty object for first
99+
// execution according to node spec
100+
if (parentModules.includes(module) && !cache) {
101+
return {};
102+
}
103+
80104
return cache
81105
? cache.exports
82-
: evalModule(module, modules, directories, externals, depth + 1);
106+
: evalModule(module, modules, directories, externals, depth + 1, [
107+
...parentModules,
108+
mainModule,
109+
]);
83110
};
84111

85112
const babelConfig = getBabelConfig(

0 commit comments

Comments
 (0)