Skip to content

Commit

Permalink
Named asset import for SVG files (#3907)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
iansu committed Feb 2, 2018
1 parent aa8789b commit d0e1731
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 55 deletions.
62 changes: 62 additions & 0 deletions packages/babel-plugin-named-asset-import/index.js
@@ -0,0 +1,62 @@
'use strict';

const { extname } = require('path');

function namedAssetImportPlugin({ types: t }) {
const visited = new WeakSet();

return {
visitor: {
ImportDeclaration(path, { opts: { loaderMap } }) {
const sourcePath = path.node.source.value;
const ext = extname(sourcePath).substr(1);

if (visited.has(path.node) || sourcePath.indexOf('!') !== -1) {
return;
}

if (loaderMap[ext]) {
path.replaceWithMultiple(
path.node.specifiers.map(specifier => {
if (t.isImportDefaultSpecifier(specifier)) {
const newDefaultImport = t.importDeclaration(
[
t.importDefaultSpecifier(
t.identifier(specifier.local.name)
),
],
t.stringLiteral(sourcePath)
);

visited.add(newDefaultImport);
return newDefaultImport;
}

const newImport = t.importDeclaration(
[
t.importSpecifier(
t.identifier(specifier.local.name),
t.identifier(specifier.imported.name)
),
],
t.stringLiteral(
loaderMap[ext][specifier.imported.name]
? loaderMap[ext][specifier.imported.name].replace(
/\[path\]/,
sourcePath
)
: sourcePath
)
);

visited.add(newImport);
return newImport;
})
);
}
},
},
};
}

module.exports = namedAssetImportPlugin;
17 changes: 17 additions & 0 deletions packages/babel-plugin-named-asset-import/package.json
@@ -0,0 +1,17 @@
{
"name": "babel-plugin-named-asset-import",
"version": "0.1.0",
"description": "Babel plugin for named asset imports in Create React App",
"repository": "facebookincubator/create-react-app",
"license": "MIT",
"bugs": {
"url": "https://github.com/facebookincubator/create-react-app/issues"
},
"main": "index.js",
"files": [
"index.js"
],
"peerDependencies": {
"@babel/core": "7.0.0-beta.38"
}
}
37 changes: 12 additions & 25 deletions packages/react-scripts/config/webpack.config.dev.js
Expand Up @@ -192,6 +192,18 @@ module.exports = {
babelrc: false,
// @remove-on-eject-end
presets: [require.resolve('babel-preset-react-app')],
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: 'svgr/webpack![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
Expand Down Expand Up @@ -266,31 +278,6 @@ module.exports = {
},
],
},
// Allows you to use two kinds of imports for SVG:
// import logoUrl from './logo.svg'; gives you the URL.
// import { ReactComponent as Logo } from './logo.svg'; gives you a component.
{
test: /\.svg$/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
// @remove-on-eject-begin
babelrc: false,
// @remove-on-eject-end
presets: [require.resolve('babel-preset-react-app')],
cacheDirectory: true,
},
},
require.resolve('svgr/webpack'),
{
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
],
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
Expand Down
37 changes: 12 additions & 25 deletions packages/react-scripts/config/webpack.config.prod.js
Expand Up @@ -200,6 +200,18 @@ module.exports = {
babelrc: false,
// @remove-on-eject-end
presets: [require.resolve('babel-preset-react-app')],
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: 'svgr/webpack![path]',
},
},
},
],
],
compact: true,
highlightCode: true,
},
Expand Down Expand Up @@ -308,31 +320,6 @@ module.exports = {
),
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
},
// Allows you to use two kinds of imports for SVG:
// import logoUrl from './logo.svg'; gives you the URL.
// import { ReactComponent as Logo } from './logo.svg'; gives you a component.
{
test: /\.svg$/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
// @remove-on-eject-begin
babelrc: false,
// @remove-on-eject-end
presets: [require.resolve('babel-preset-react-app')],
cacheDirectory: true,
},
},
require.resolve('svgr/webpack'),
{
loader: require.resolve('file-loader'),
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
],
},
// "file" loader makes sure assets end up in the `build` folder.
// When you `import` an asset, you get its filename.
// This loader doesn't use a "test" so it will catch all modules
Expand Down
Expand Up @@ -71,6 +71,22 @@ describe('Integration', () => {
);
});

it('svg component', async () => {
const doc = await initDOM('svg-component');

expect(doc.getElementById('feature-svg-component').textContent).to.equal(
''
);
});

it('svg in css', async () => {
const doc = await initDOM('svg-in-css');

expect(
doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '')
).to.match(/\/static\/media\/logo\..+\.svg/);
});

it('unknown ext inclusion', async () => {
const doc = await initDOM('unknown-ext-inclusion');

Expand Down
16 changes: 13 additions & 3 deletions packages/react-scripts/fixtures/kitchensink/src/App.js
Expand Up @@ -82,9 +82,9 @@ class App extends Component {
);
break;
case 'css-modules-inclusion':
import(
'./features/webpack/CssModulesInclusion'
).then(f => this.setFeature(f.default));
import('./features/webpack/CssModulesInclusion').then(f =>
this.setFeature(f.default)
);
break;
case 'custom-interpolation':
import('./features/syntax/CustomInterpolation').then(f =>
Expand Down Expand Up @@ -174,6 +174,16 @@ class App extends Component {
this.setFeature(f.default)
);
break;
case 'svg-component':
import('./features/webpack/SvgComponent').then(f =>
this.setFeature(f.default)
);
break;
case 'svg-in-css':
import('./features/webpack/SvgInCss').then(f =>
this.setFeature(f.default)
);
break;
case 'template-interpolation':
import('./features/syntax/TemplateInterpolation').then(f =>
this.setFeature(f.default)
Expand Down
Expand Up @@ -8,4 +8,4 @@
import React from 'react';
import { ReactComponent as Logo } from './assets/logo.svg';

export default () => <Logo />;
export default () => <Logo id="feature-svg-component" />;
@@ -0,0 +1,4 @@
import React from 'react';
import './assets/svg.css';

export default () => <div id="feature-svg-in-css" />;
@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom';
import SvgInCss from './SvgInCss';

describe('svg in css', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<SvgInCss />, div);
});
});
@@ -0,0 +1,3 @@
#feature-svg-in-css {
background-image: url("./logo.svg");
}
3 changes: 2 additions & 1 deletion packages/react-scripts/package.json
Expand Up @@ -28,6 +28,7 @@
"babel-eslint": "8.2.1",
"babel-jest": "22.1.0",
"babel-loader": "8.0.0-beta.0",
"babel-plugin-named-asset-import": "^0.1.0",
"babel-preset-react-app": "^3.1.1",
"case-sensitive-paths-webpack-plugin": "2.1.1",
"chalk": "2.3.0",
Expand Down Expand Up @@ -56,7 +57,7 @@
"raf": "3.4.0",
"react-dev-utils": "^5.0.0",
"style-loader": "0.19.1",
"svgr": "1.6.0",
"svgr": "1.8.1",
"sw-precache-webpack-plugin": "0.11.4",
"thread-loader": "1.1.2",
"uglifyjs-webpack-plugin": "1.1.6",
Expand Down

0 comments on commit d0e1731

Please sign in to comment.