diff --git a/packages/babel-plugin-named-asset-import/index.js b/packages/babel-plugin-named-asset-import/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6fd919bc6767d778e81736dcbcf4dddfb018df91 --- /dev/null +++ b/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; diff --git a/packages/babel-plugin-named-asset-import/package.json b/packages/babel-plugin-named-asset-import/package.json new file mode 100644 index 0000000000000000000000000000000000000000..9c586ac57535c8caec946d1732949343c7e55e30 --- /dev/null +++ b/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" + } +} diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 66f07baa283498bfe4e13af224554019f9343b18..bd5d218cc8d1b7445c6ca0a81811f68fba0b5c1e 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -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. @@ -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. diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 1d9a617c8291575b74dea7d0ec3a99be5cb762a3..6c9a879e8518fe34befff7fe11634680ce72999c 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -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, }, @@ -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 diff --git a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js index 06ec83602f36bf6fdb13cdf68da5d7be5f0b2b20..e479be4b81ae48337e16654704541d9b2c685d1e 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js @@ -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'); diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js index d1affb48af9e885102619a08744f8b5679553a9c..750f8a90b986dec27b452b5e4fc40935eed3805c 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/App.js +++ b/packages/react-scripts/fixtures/kitchensink/src/App.js @@ -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 => @@ -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) diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgComponent.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgComponent.js index 0eb06a027e3e5b63e3ffc3f3843105e56d69a58b..62bad3b1075112ae49632b1397cdf4a15d9efb30 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgComponent.js +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgComponent.js @@ -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" />; diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgInCss.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgInCss.js new file mode 100644 index 0000000000000000000000000000000000000000..edf34137940ca25d49721ba796a07e231dbd06f0 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgInCss.js @@ -0,0 +1,4 @@ +import React from 'react'; +import './assets/svg.css'; + +export default () => <div id="feature-svg-in-css" />; diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgInCss.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgInCss.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f0c0bd68372e9834f57d6007153d42a323e5394f --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SvgInCss.test.js @@ -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); + }); +}); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/svg.css b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/svg.css new file mode 100644 index 0000000000000000000000000000000000000000..ad0ff93655aedc5c5730254015b51caba011ac83 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/svg.css @@ -0,0 +1,3 @@ +#feature-svg-in-css { + background-image: url("./logo.svg"); +} diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 9bb5a1a07589750302b431f17cdfac54d39be728..fd876f502f27abc55fb5f399259b72a984d6ba8a 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -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", @@ -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",