Skip to content

Commit d0e1731

Browse files
authored
Named asset import for SVG files (facebook#3907)
* Add named asset import for svg files via babel plugin and webpack loader. * Fix failing e2e test * Switched to svgr loader * Updated SVG component test * Disable named asset import plugin in test environment * Added tests for including SVG in CSS * Update tests * Moved babel plugin config into webpack config
1 parent aa8789b commit d0e1731

File tree

11 files changed

+152
-55
lines changed

11 files changed

+152
-55
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
const { extname } = require('path');
4+
5+
function namedAssetImportPlugin({ types: t }) {
6+
const visited = new WeakSet();
7+
8+
return {
9+
visitor: {
10+
ImportDeclaration(path, { opts: { loaderMap } }) {
11+
const sourcePath = path.node.source.value;
12+
const ext = extname(sourcePath).substr(1);
13+
14+
if (visited.has(path.node) || sourcePath.indexOf('!') !== -1) {
15+
return;
16+
}
17+
18+
if (loaderMap[ext]) {
19+
path.replaceWithMultiple(
20+
path.node.specifiers.map(specifier => {
21+
if (t.isImportDefaultSpecifier(specifier)) {
22+
const newDefaultImport = t.importDeclaration(
23+
[
24+
t.importDefaultSpecifier(
25+
t.identifier(specifier.local.name)
26+
),
27+
],
28+
t.stringLiteral(sourcePath)
29+
);
30+
31+
visited.add(newDefaultImport);
32+
return newDefaultImport;
33+
}
34+
35+
const newImport = t.importDeclaration(
36+
[
37+
t.importSpecifier(
38+
t.identifier(specifier.local.name),
39+
t.identifier(specifier.imported.name)
40+
),
41+
],
42+
t.stringLiteral(
43+
loaderMap[ext][specifier.imported.name]
44+
? loaderMap[ext][specifier.imported.name].replace(
45+
/\[path\]/,
46+
sourcePath
47+
)
48+
: sourcePath
49+
)
50+
);
51+
52+
visited.add(newImport);
53+
return newImport;
54+
})
55+
);
56+
}
57+
},
58+
},
59+
};
60+
}
61+
62+
module.exports = namedAssetImportPlugin;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "babel-plugin-named-asset-import",
3+
"version": "0.1.0",
4+
"description": "Babel plugin for named asset imports in Create React App",
5+
"repository": "facebookincubator/create-react-app",
6+
"license": "MIT",
7+
"bugs": {
8+
"url": "https://github.com/facebookincubator/create-react-app/issues"
9+
},
10+
"main": "index.js",
11+
"files": [
12+
"index.js"
13+
],
14+
"peerDependencies": {
15+
"@babel/core": "7.0.0-beta.38"
16+
}
17+
}

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

+12-25
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,18 @@ module.exports = {
192192
babelrc: false,
193193
// @remove-on-eject-end
194194
presets: [require.resolve('babel-preset-react-app')],
195+
plugins: [
196+
[
197+
require.resolve('babel-plugin-named-asset-import'),
198+
{
199+
loaderMap: {
200+
svg: {
201+
ReactComponent: 'svgr/webpack![path]',
202+
},
203+
},
204+
},
205+
],
206+
],
195207
// This is a feature of `babel-loader` for webpack (not Babel itself).
196208
// It enables caching results in ./node_modules/.cache/babel-loader/
197209
// directory for faster rebuilds.
@@ -266,31 +278,6 @@ module.exports = {
266278
},
267279
],
268280
},
269-
// Allows you to use two kinds of imports for SVG:
270-
// import logoUrl from './logo.svg'; gives you the URL.
271-
// import { ReactComponent as Logo } from './logo.svg'; gives you a component.
272-
{
273-
test: /\.svg$/,
274-
use: [
275-
{
276-
loader: require.resolve('babel-loader'),
277-
options: {
278-
// @remove-on-eject-begin
279-
babelrc: false,
280-
// @remove-on-eject-end
281-
presets: [require.resolve('babel-preset-react-app')],
282-
cacheDirectory: true,
283-
},
284-
},
285-
require.resolve('svgr/webpack'),
286-
{
287-
loader: require.resolve('file-loader'),
288-
options: {
289-
name: 'static/media/[name].[hash:8].[ext]',
290-
},
291-
},
292-
],
293-
},
294281
// "file" loader makes sure those assets get served by WebpackDevServer.
295282
// When you `import` an asset, you get its (virtual) filename.
296283
// In production, they would get copied to the `build` folder.

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

+12-25
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,18 @@ module.exports = {
200200
babelrc: false,
201201
// @remove-on-eject-end
202202
presets: [require.resolve('babel-preset-react-app')],
203+
plugins: [
204+
[
205+
require.resolve('babel-plugin-named-asset-import'),
206+
{
207+
loaderMap: {
208+
svg: {
209+
ReactComponent: 'svgr/webpack![path]',
210+
},
211+
},
212+
},
213+
],
214+
],
203215
compact: true,
204216
highlightCode: true,
205217
},
@@ -308,31 +320,6 @@ module.exports = {
308320
),
309321
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
310322
},
311-
// Allows you to use two kinds of imports for SVG:
312-
// import logoUrl from './logo.svg'; gives you the URL.
313-
// import { ReactComponent as Logo } from './logo.svg'; gives you a component.
314-
{
315-
test: /\.svg$/,
316-
use: [
317-
{
318-
loader: require.resolve('babel-loader'),
319-
options: {
320-
// @remove-on-eject-begin
321-
babelrc: false,
322-
// @remove-on-eject-end
323-
presets: [require.resolve('babel-preset-react-app')],
324-
cacheDirectory: true,
325-
},
326-
},
327-
require.resolve('svgr/webpack'),
328-
{
329-
loader: require.resolve('file-loader'),
330-
options: {
331-
name: 'static/media/[name].[hash:8].[ext]',
332-
},
333-
},
334-
],
335-
},
336323
// "file" loader makes sure assets end up in the `build` folder.
337324
// When you `import` an asset, you get its filename.
338325
// This loader doesn't use a "test" so it will catch all modules

packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js

+16
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ describe('Integration', () => {
7171
);
7272
});
7373

74+
it('svg component', async () => {
75+
const doc = await initDOM('svg-component');
76+
77+
expect(doc.getElementById('feature-svg-component').textContent).to.equal(
78+
''
79+
);
80+
});
81+
82+
it('svg in css', async () => {
83+
const doc = await initDOM('svg-in-css');
84+
85+
expect(
86+
doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '')
87+
).to.match(/\/static\/media\/logo\..+\.svg/);
88+
});
89+
7490
it('unknown ext inclusion', async () => {
7591
const doc = await initDOM('unknown-ext-inclusion');
7692

packages/react-scripts/fixtures/kitchensink/src/App.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ class App extends Component {
8282
);
8383
break;
8484
case 'css-modules-inclusion':
85-
import(
86-
'./features/webpack/CssModulesInclusion'
87-
).then(f => this.setFeature(f.default));
85+
import('./features/webpack/CssModulesInclusion').then(f =>
86+
this.setFeature(f.default)
87+
);
8888
break;
8989
case 'custom-interpolation':
9090
import('./features/syntax/CustomInterpolation').then(f =>
@@ -174,6 +174,16 @@ class App extends Component {
174174
this.setFeature(f.default)
175175
);
176176
break;
177+
case 'svg-component':
178+
import('./features/webpack/SvgComponent').then(f =>
179+
this.setFeature(f.default)
180+
);
181+
break;
182+
case 'svg-in-css':
183+
import('./features/webpack/SvgInCss').then(f =>
184+
this.setFeature(f.default)
185+
);
186+
break;
177187
case 'template-interpolation':
178188
import('./features/syntax/TemplateInterpolation').then(f =>
179189
this.setFeature(f.default)

packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgComponent.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
import React from 'react';
99
import { ReactComponent as Logo } from './assets/logo.svg';
1010

11-
export default () => <Logo />;
11+
export default () => <Logo id="feature-svg-component" />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import React from 'react';
2+
import './assets/svg.css';
3+
4+
export default () => <div id="feature-svg-in-css" />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import SvgInCss from './SvgInCss';
4+
5+
describe('svg in css', () => {
6+
it('renders without crashing', () => {
7+
const div = document.createElement('div');
8+
ReactDOM.render(<SvgInCss />, div);
9+
});
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#feature-svg-in-css {
2+
background-image: url("./logo.svg");
3+
}

packages/react-scripts/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"babel-eslint": "8.2.1",
2929
"babel-jest": "22.1.0",
3030
"babel-loader": "8.0.0-beta.0",
31+
"babel-plugin-named-asset-import": "^0.1.0",
3132
"babel-preset-react-app": "^3.1.1",
3233
"case-sensitive-paths-webpack-plugin": "2.1.1",
3334
"chalk": "2.3.0",
@@ -56,7 +57,7 @@
5657
"raf": "3.4.0",
5758
"react-dev-utils": "^5.0.0",
5859
"style-loader": "0.19.1",
59-
"svgr": "1.6.0",
60+
"svgr": "1.8.1",
6061
"sw-precache-webpack-plugin": "0.11.4",
6162
"thread-loader": "1.1.2",
6263
"uglifyjs-webpack-plugin": "1.1.6",

0 commit comments

Comments
 (0)