Skip to content

Commit 225a8e8

Browse files
authored
Custom babel config support (codesandbox#16)
* Custom babel config support * Remove sandboxId dependency * Remove duplicate import * Add proper babel resolving with tests * Make default babel config a static * Use strings as plugins * Set custom babel config support behind a feature flag
1 parent 46efcf3 commit 225a8e8

File tree

9 files changed

+267
-41
lines changed

9 files changed

+267
-41
lines changed

src/app/components/sandbox/Preview/index.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type Props = {
3232
isInProjectView: boolean,
3333
modules: Array<Module>,
3434
directories: Array<Directory>,
35-
bundle: Sandbox.dependencyBundle,
35+
bundle: typeof Sandbox.dependencyBundle,
3636
externalResources: typeof Sandbox.externalResources,
3737
preferences: Preferences,
3838
fetchBundle: (id: string) => Object,
@@ -201,7 +201,6 @@ export default class Preview extends React.PureComponent {
201201
directories,
202202
bundle = {},
203203
module,
204-
sandboxId,
205204
externalResources,
206205
preferences,
207206
} = this.props;
@@ -222,7 +221,6 @@ export default class Preview extends React.PureComponent {
222221
boilerplates: defaultBoilerplates,
223222
module: renderedModule,
224223
changedModule: module,
225-
sandboxId,
226224
modules,
227225
directories,
228226
externals: bundle.externals,
@@ -231,7 +229,7 @@ export default class Preview extends React.PureComponent {
231229
});
232230
};
233231

234-
addError = (e: { moduleId: string, message: string, line: number }) => {
232+
addError = (e: ModuleError) => {
235233
this.props.addError(this.props.sandboxId, e);
236234
};
237235

src/sandbox/errors/dependency-not-found-error.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ const parseDependencyName = (dependency: string) => {
88
export default class DependencyNotFoundError extends Error {
99
constructor(dependencyName: string) {
1010
super();
11+
12+
const parsedName = parseDependencyName(dependencyName);
1113
this.payload = {
12-
dependency: parseDependencyName(dependencyName),
14+
dependency: parsedName,
1315
path: dependencyName,
1416
};
17+
this.message = `Could not find dependency: '${parsedName}'`;
1518
}
1619
type = 'dependency-not-found';
1720
severity = 'error';

src/sandbox/eval/css.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default module => {
2626
// const alteredClassNames = getGeneratedClassNames(module.id, classNames);
2727

2828
// const newCode = getGeneratedClassNameCode(module.code, alteredClassNames);
29-
createStyleNode(`${module.id}`, css);
29+
createStyleNode(module.id, css);
3030

3131
return classNames;
3232
};

src/sandbox/eval/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import evalJson from './json';
88

99
const MAX_DEPTH = 20;
1010

11-
function doEval(mainModule, modules, directories, externals, depth) {
11+
function doEval(
12+
mainModule: Module,
13+
modules: Array<Module>,
14+
directories: Array<Directory>,
15+
externals: Object,
16+
depth: ?number,
17+
) {
1218
const html = /\.html$/;
1319
const css = /\.css$/;
1420
const json = /\.json$/;
@@ -22,7 +28,7 @@ function doEval(mainModule, modules, directories, externals, depth) {
2228
return evalCSS(mainModule, modules, directories, externals, depth);
2329
}
2430

25-
if (json.test(mainModule.title)) {
31+
if (json.test(mainModule.title) || mainModule.title === '.babelrc') {
2632
return evalJson(mainModule, modules, directories, externals, depth);
2733
}
2834

src/sandbox/eval/index.test.js

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('eval', () => {
1111
`,
1212
};
1313

14-
expect(evaller(mainModule)).toEqual({ default: 3 });
14+
expect(evaller(mainModule, [], [])).toEqual({ default: 3 });
1515
});
1616

1717
test('multiple es exports', () => {
@@ -24,7 +24,11 @@ describe('eval', () => {
2424
`,
2525
};
2626

27-
expect(evaller(mainModule)).toEqual({ a: 'b', b: 'c', default: 3 });
27+
expect(evaller(mainModule, [], [])).toEqual({
28+
a: 'b',
29+
b: 'c',
30+
default: 3,
31+
});
2832
});
2933

3034
test('node exports', () => {
@@ -35,7 +39,7 @@ describe('eval', () => {
3539
`,
3640
};
3741

38-
expect(evaller(mainModule)).toEqual(3);
42+
expect(evaller(mainModule, [], [])).toEqual(3);
3943
});
4044

4145
test('imports', () => {
@@ -55,10 +59,105 @@ describe('eval', () => {
5559
`,
5660
};
5761

58-
expect(evaller(mainModule, [mainModule, secondModule])).toEqual({
62+
expect(evaller(mainModule, [mainModule, secondModule], [])).toEqual({
5963
default: 3,
6064
});
6165
});
66+
67+
describe.skip('custom babel config', () => {
68+
it('uses custom babel config', () => {
69+
const mainModule = {
70+
title: 'test.js',
71+
shortid: '1',
72+
code: `
73+
const a = {b: 'a'};
74+
const b = {a: 'b'};
75+
export default {...a, ...b};
76+
`,
77+
};
78+
79+
const babelConfig = {
80+
title: '.babelrc',
81+
shortid: '2',
82+
code: `
83+
{
84+
"presets": ["es2015", "react", "stage-0"]
85+
}
86+
`,
87+
};
88+
89+
expect(evaller(mainModule, [mainModule, babelConfig], [])).toEqual({
90+
default: { a: 'b', b: 'a' },
91+
});
92+
93+
const emptyBabelConfig = {
94+
title: '.babelrc',
95+
shortid: '2',
96+
code: `
97+
{
98+
"presets": []
99+
}`,
100+
};
101+
102+
expect(() =>
103+
evaller(mainModule, [mainModule, emptyBabelConfig], []),
104+
).toThrow();
105+
});
106+
107+
it('resolves to dependencies as plugins', () => {
108+
const mainModule = {
109+
title: 'test.js',
110+
shortid: '1',
111+
code: `
112+
const a = {b: 'a'};
113+
const b = {a: 'b'};
114+
export default {...a, ...b};
115+
`,
116+
};
117+
118+
const babelConfig = {
119+
title: '.babelrc',
120+
shortid: '2',
121+
code: `
122+
{
123+
"presets": ["es2015", "react", "stage-0"],
124+
"plugins": ["emotion/babel"]
125+
}
126+
`,
127+
};
128+
129+
expect(() =>
130+
evaller(mainModule, [mainModule, babelConfig], [], {}),
131+
).toThrowError("Could not find dependency: 'emotion'");
132+
});
133+
134+
it('can resolve plugins with options', () => {
135+
const mainModule = {
136+
title: 'test.js',
137+
shortid: '1',
138+
code: `
139+
const a = {b: 'a'};
140+
const b = {a: 'b'};
141+
export default {...a, ...b};
142+
`,
143+
};
144+
145+
const babelConfig = {
146+
title: '.babelrc',
147+
shortid: '2',
148+
code: `
149+
{
150+
"presets": ["es2015", "react", "stage-0"],
151+
"plugins": [["emotion/babel", { "inline": true }]]
152+
}
153+
`,
154+
};
155+
156+
expect(() =>
157+
evaller(mainModule, [mainModule, babelConfig], [], {}),
158+
).toThrowError("Could not find dependency: 'emotion'");
159+
});
160+
});
62161
});
63162

64163
test('css', () => {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// @flow
2+
import type { Module, Directory } from 'common/types';
3+
4+
import 'babel-plugin-transform-async-to-generator';
5+
import 'babel-plugin-transform-object-rest-spread';
6+
import 'babel-plugin-transform-class-properties';
7+
import 'babel-plugin-transform-decorators-legacy';
8+
9+
import evalModule from '../';
10+
import resolveDependency from './dependency-resolver';
11+
import DependencyNotFoundError from '../../errors/dependency-not-found-error';
12+
13+
const CUSTOM_BABEL_CONFIG_ENABLED = false;
14+
15+
const DEFAULT_BABEL_CONFIG = {
16+
presets: ['es2015', 'react', 'stage-0'],
17+
plugins: [
18+
'transform-decorators-legacy',
19+
'transform-async-to-generator',
20+
'transform-object-rest-spread',
21+
'transform-class-properties',
22+
],
23+
retainLines: true,
24+
};
25+
26+
const resolvePlugin = (plugin: string, externals) => {
27+
const resolvedPlugin =
28+
resolveDependency(plugin, externals) ||
29+
resolveDependency(`babel-plugin-${plugin}`, externals);
30+
31+
if (!resolvedPlugin) throw new DependencyNotFoundError(plugin);
32+
};
33+
34+
/**
35+
* Rewrite the plugin strings to actual dependencies of a babel config
36+
*/
37+
function rewritePlugins(plugins: ?Array<string>, externals) {
38+
if (plugins == null) return [];
39+
40+
return plugins.map(dependency => {
41+
if (typeof dependency === 'string') {
42+
return resolvePlugin(dependency, externals);
43+
} else if (Array.isArray(dependency)) {
44+
const newDependency = [...dependency];
45+
newDependency[0] = resolvePlugin(dependency[0], externals);
46+
47+
return newDependency;
48+
}
49+
50+
throw new Error(
51+
`Could not parse babel plugin: '${JSON.stringify(dependency)}'`,
52+
);
53+
});
54+
}
55+
56+
/**
57+
* Parses the .babelrc if it exists, if it doesn't it will return a default config
58+
*/
59+
export default function getBabelConfig(
60+
currentModule: Module,
61+
modules: Array<Module>,
62+
directories: Array<Directory>,
63+
externals: Object,
64+
depth: number,
65+
) {
66+
const babelConfigModule = modules.find(
67+
m => m.title === '.babelrc' && !m.directoryShortid,
68+
);
69+
70+
if (
71+
babelConfigModule &&
72+
babelConfigModule !== currentModule &&
73+
CUSTOM_BABEL_CONFIG_ENABLED
74+
) {
75+
const config = evalModule(
76+
babelConfigModule,
77+
modules,
78+
directories,
79+
externals,
80+
depth,
81+
);
82+
83+
const resolvedConfig = {
84+
...config,
85+
plugins: rewritePlugins(config.plugins, externals),
86+
retainLines: true,
87+
};
88+
89+
return resolvedConfig;
90+
}
91+
92+
return DEFAULT_BABEL_CONFIG;
93+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Converts a dependency string to an actual dependency, this can return null
3+
* if the dependency is not found.
4+
*
5+
* @param {string} dependencyPath
6+
* @param {Object} externals
7+
* @returns
8+
*/
9+
export default function getDependency(
10+
dependencyPath: string,
11+
externals: { [key: string]: string },
12+
) {
13+
const dependencyModule =
14+
externals[dependencyPath] || externals[`${dependencyPath}.js`];
15+
if (dependencyModule) {
16+
const idMatch = dependencyModule.match(/dll_bundle\((\d+)\)/);
17+
if (idMatch && idMatch[1]) {
18+
try {
19+
return window.dll_bundle(idMatch[1]);
20+
} catch (e) {
21+
// Delete the cache of the throwing dependency
22+
delete window.dll_bundle.c[idMatch[1]];
23+
throw e;
24+
}
25+
}
26+
}
27+
28+
return null;
29+
}

0 commit comments

Comments
 (0)