Skip to content

Commit f8a1faa

Browse files
committed
Basic plugin support
1 parent 0482058 commit f8a1faa

File tree

5 files changed

+330
-9
lines changed

5 files changed

+330
-9
lines changed

packages/react-dev-utils/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"ModuleScopePlugin.js",
2828
"openBrowser.js",
2929
"openChrome.applescript",
30+
"plugins.js",
3031
"printHostingInstructions.js",
3132
"WatchMissingNodeModulesPlugin.js",
3233
"WebpackDevServerUtils.js",
@@ -36,6 +37,11 @@
3637
"address": "1.0.2",
3738
"anser": "1.4.1",
3839
"babel-code-frame": "6.22.0",
40+
"babel-generator": "^6.25.0",
41+
"babel-template": "^6.25.0",
42+
"babel-traverse": "^6.25.0",
43+
"babel-types": "^6.25.0",
44+
"babylon": "^6.17.4",
3945
"chalk": "1.1.3",
4046
"cross-spawn": "4.0.2",
4147
"detect-port-alt": "1.1.3",
@@ -47,7 +53,10 @@
4753
"inquirer": "3.1.1",
4854
"is-root": "1.0.0",
4955
"opn": "5.1.0",
56+
"prettier": "^1.5.2",
57+
"read-pkg-up": "^2.0.0",
5058
"recursive-readdir": "2.2.1",
59+
"semver": "^5.3.0",
5160
"shell-quote": "1.6.1",
5261
"sockjs-client": "1.1.4",
5362
"strip-ansi": "3.0.1",

packages/react-dev-utils/plugins.js

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
'use strict';
11+
12+
const babylon = require('babylon');
13+
const traverse = require('babel-traverse').default;
14+
const template = require('babel-template');
15+
const generator = require('babel-generator').default;
16+
const t = require('babel-types');
17+
const { readFileSync } = require('fs');
18+
const prettier = require('prettier');
19+
const getPackageJson = require('read-pkg-up').sync;
20+
const { dirname, isAbsolute } = require('path');
21+
const semver = require('semver');
22+
23+
function applyPlugins(config, plugins, { paths }) {
24+
const pluginPaths = plugins
25+
.map(p => {
26+
try {
27+
return require.resolve(`react-scripts-plugin-${p}`);
28+
} catch (e) {
29+
return null;
30+
}
31+
})
32+
.filter(e => e != null);
33+
for (const pluginPath of pluginPaths) {
34+
const { apply } = require(pluginPath);
35+
config = apply(config, { paths });
36+
}
37+
return config;
38+
}
39+
40+
function _getArrayValues(arr) {
41+
const { elements } = arr;
42+
return elements.map(e => {
43+
if (e.type === 'StringLiteral') {
44+
return e.value;
45+
}
46+
return e;
47+
});
48+
}
49+
50+
// arr: [[afterExt, strExt1, strExt2, ...], ...]
51+
function pushExtensions({ config, ast }, arr) {
52+
if (ast != null) {
53+
traverse(ast, {
54+
enter(path) {
55+
const { type } = path;
56+
if (type !== 'ArrayExpression') {
57+
return;
58+
}
59+
const { key } = path.parent;
60+
if (key == null || key.name !== 'extensions') {
61+
return;
62+
}
63+
const { elements } = path.node;
64+
const extensions = _getArrayValues(path.node);
65+
for (const [after, ...exts] of arr) {
66+
// Find the extension we want to add after
67+
const index = extensions.findIndex(s => s === after);
68+
if (index === -1) {
69+
throw new Error(
70+
`Unable to find extension ${after} in configuration.`
71+
);
72+
}
73+
// Push the extensions into array in the order we specify
74+
elements.splice(
75+
index + 1,
76+
0,
77+
...exts.map(ext => t.stringLiteral(ext))
78+
);
79+
// Simulate into our local copy of the array to keep proper indices
80+
extensions.splice(index + 1, 0, ...exts);
81+
}
82+
},
83+
});
84+
} else if (config != null) {
85+
const { resolve: { extensions } } = config;
86+
87+
for (const [after, ...exts] of arr) {
88+
// Find the extension we want to add after
89+
const index = extensions.findIndex(s => s === after);
90+
if (index === -1) {
91+
throw new Error(`Unable to find extension ${after} in configuration.`);
92+
}
93+
// Push the extensions into array in the order we specify
94+
extensions.splice(index + 1, 0, ...exts);
95+
}
96+
}
97+
}
98+
99+
function pushExclusiveLoader({ config, ast }, testStr, loader) {
100+
if (ast != null) {
101+
traverse(ast, {
102+
enter(path) {
103+
const { type } = path;
104+
if (type !== 'ArrayExpression') {
105+
return;
106+
}
107+
const { key } = path.parent;
108+
if (key == null || key.name !== 'oneOf') {
109+
return;
110+
}
111+
const entries = _getArrayValues(path.node);
112+
const afterIndex = entries.findIndex(entry => {
113+
const { properties } = entry;
114+
return (
115+
properties.find(property => {
116+
if (property.value.type !== 'RegExpLiteral') {
117+
return false;
118+
}
119+
return property.value.pattern === testStr.slice(1, -1);
120+
}) != null
121+
);
122+
});
123+
if (afterIndex === -1) {
124+
throw new Error('Unable to match pre-loader.');
125+
}
126+
path.node.elements.splice(afterIndex + 1, 0, loader);
127+
},
128+
});
129+
} else if (config != null) {
130+
const { module: { rules: [, { oneOf: rules }] } } = config;
131+
const loaderIndex = rules.findIndex(
132+
rule => rule.test.toString() === testStr
133+
);
134+
if (loaderIndex === -1) {
135+
throw new Error('Unable to match pre-loader.');
136+
}
137+
rules.splice(loaderIndex + 1, 0, loader);
138+
}
139+
}
140+
141+
function ejectFile({ filename, code, existingDependencies }) {
142+
if (filename != null) {
143+
code = readFileSync(filename, 'utf8');
144+
}
145+
let ast = babylon.parse(code);
146+
147+
let plugins = [];
148+
traverse(ast, {
149+
enter(path) {
150+
const { type } = path;
151+
if (type === 'VariableDeclaration') {
152+
const { node: { declarations: [{ id: { name }, init }] } } = path;
153+
if (name !== 'base') {
154+
return;
155+
}
156+
path.replaceWith(template('module.exports = RIGHT;')({ RIGHT: init }));
157+
} else if (type === 'AssignmentExpression') {
158+
const { node: { left, right } } = path;
159+
if (left.type !== 'MemberExpression') {
160+
return;
161+
}
162+
if (right.type !== 'CallExpression') {
163+
return;
164+
}
165+
const { callee: { name }, arguments: args } = right;
166+
if (name !== 'applyPlugins') {
167+
return;
168+
}
169+
plugins = _getArrayValues(args[1]);
170+
path.parentPath.remove();
171+
}
172+
},
173+
});
174+
let deferredTransforms = [];
175+
const dependencies = new Map([...existingDependencies]);
176+
const paths = new Set();
177+
plugins.forEach(p => {
178+
let path;
179+
try {
180+
path = require.resolve(`react-scripts-plugin-${p}`);
181+
} catch (e) {
182+
return;
183+
}
184+
paths.add(path);
185+
186+
const { pkg: pluginPackage } = getPackageJson({ cwd: dirname(path) });
187+
for (const pkg of Object.keys(pluginPackage.dependencies)) {
188+
const version = pluginPackage.dependencies[pkg];
189+
if (dependencies.has(pkg)) {
190+
const prev = dependencies.get(pkg);
191+
if (
192+
isAbsolute(version) ||
193+
semver.satisfies(version.replace(/[\^~]/g, ''), prev)
194+
) {
195+
continue;
196+
} else if (!semver.satisfies(prev.replace(/[\^~]/g, ''), version)) {
197+
throw new Error(
198+
`Dependency ${pkg}@${version} cannot be satisfied by colliding range ${pkg}@${prev}.`
199+
);
200+
}
201+
}
202+
dependencies.set(pkg, pluginPackage.dependencies[pkg]);
203+
}
204+
205+
const pluginCode = readFileSync(path, 'utf8');
206+
const pluginAst = babylon.parse(pluginCode);
207+
traverse(pluginAst, {
208+
enter(path) {
209+
const { type } = path;
210+
if (type !== 'CallExpression') {
211+
return;
212+
}
213+
const { node: { callee: { name }, arguments: pluginArgs } } = path;
214+
switch (name) {
215+
case 'pushExtensions': {
216+
const [, _exts] = pluginArgs;
217+
const exts = _getArrayValues(_exts).map(entry =>
218+
_getArrayValues(entry)
219+
);
220+
deferredTransforms.push(
221+
pushExtensions.bind(undefined, { ast }, exts)
222+
);
223+
break;
224+
}
225+
case 'pushExclusiveLoader': {
226+
const [, { value: testStr }, _loader] = pluginArgs;
227+
deferredTransforms.push(
228+
pushExclusiveLoader.bind(undefined, { ast }, testStr, _loader)
229+
);
230+
break;
231+
}
232+
default: {
233+
// Not a call we care about
234+
break;
235+
}
236+
}
237+
},
238+
});
239+
});
240+
// Execute 'em!
241+
for (const transform of deferredTransforms) {
242+
transform();
243+
}
244+
let { code: outCode } = generator(
245+
ast,
246+
{ sourceMaps: false, comments: true, retainLines: false },
247+
code
248+
);
249+
outCode = prettier.format(outCode, {
250+
singleQuote: true,
251+
trailingComma: 'es5',
252+
});
253+
254+
return { code: outCode, dependencies, paths };
255+
}
256+
257+
module.exports = {
258+
applyPlugins,
259+
pushExtensions,
260+
pushExclusiveLoader,
261+
ejectFile,
262+
};

packages/react-scripts/config/webpack.config.dev.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const eslintFormatter = require('react-dev-utils/eslintFormatter');
2121
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
2222
const getClientEnvironment = require('./env');
2323
const paths = require('./paths');
24+
const { applyPlugins } = require('react-dev-utils/plugins');
2425

2526
// Webpack uses `publicPath` to determine where the app is being served from.
2627
// In development, we always serve from the root. This makes config easier.
@@ -35,7 +36,7 @@ const env = getClientEnvironment(publicUrl);
3536
// This is the development configuration.
3637
// It is focused on developer experience and fast rebuilds.
3738
// The production configuration is different and lives in a separate file.
38-
module.exports = {
39+
const base = {
3940
// You may want 'eval' instead if you prefer to see the compiled output in DevTools.
4041
// See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.
4142
devtool: 'cheap-module-source-map',
@@ -290,3 +291,5 @@ module.exports = {
290291
hints: false,
291292
},
292293
};
294+
295+
module.exports = applyPlugins(base, [], { paths });

packages/react-scripts/config/webpack.config.prod.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const eslintFormatter = require('react-dev-utils/eslintFormatter');
2222
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
2323
const paths = require('./paths');
2424
const getClientEnvironment = require('./env');
25+
const { applyPlugins } = require('react-dev-utils/plugins');
2526

2627
// Webpack uses `publicPath` to determine where the app is being served from.
2728
// It requires a trailing slash, or the file assets will get an incorrect path.
@@ -57,7 +58,7 @@ const extractTextPluginOptions = shouldUseRelativeAssetPaths
5758
// This is the production configuration.
5859
// It compiles slowly and is focused on producing a fast and minimal bundle.
5960
// The development configuration is different and lives in a separate file.
60-
module.exports = {
61+
const base = {
6162
// Don't attempt to continue if there are any errors.
6263
bail: true,
6364
// We generate sourcemaps in production. This is slow but gives good results.
@@ -358,3 +359,5 @@ module.exports = {
358359
tls: 'empty',
359360
},
360361
};
362+
363+
module.exports = applyPlugins(base, [], { paths });

0 commit comments

Comments
 (0)