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 00000000000..6fd919bc676 --- /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 00000000000..9c586ac5753 --- /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-dev-utils/launchEditor.js b/packages/react-dev-utils/launchEditor.js index 933fc1d4d30..55b8e22dc83 100644 --- a/packages/react-dev-utils/launchEditor.js +++ b/packages/react-dev-utils/launchEditor.js @@ -57,6 +57,8 @@ const COMMON_EDITORS_OSX = { '/Applications/WebStorm.app/Contents/MacOS/webstorm': '/Applications/WebStorm.app/Contents/MacOS/webstorm', '/Applications/MacVim.app/Contents/MacOS/MacVim': 'mvim', + '/Applications/GoLand.app/Contents/MacOS/goland': + '/Applications/GoLand.app/Contents/MacOS/goland', }; const COMMON_EDITORS_LINUX = { @@ -72,6 +74,7 @@ const COMMON_EDITORS_LINUX = { sublime_text: 'sublime_text', vim: 'vim', 'webstorm.sh': 'webstorm', + 'goland.sh': 'goland', }; const COMMON_EDITORS_WIN = [ @@ -93,6 +96,8 @@ const COMMON_EDITORS_WIN = [ 'rubymine64.exe', 'webstorm.exe', 'webstorm64.exe', + 'goland.exe', + 'goland64.exe', ]; function addWorkspaceToArgumentsIfExists(args, workspace) { @@ -155,6 +160,8 @@ function getArgumentsForLineNumber( case 'rubymine64': case 'webstorm': case 'webstorm64': + case 'goland': + case 'goland64': return addWorkspaceToArgumentsIfExists( ['--line', lineNumber, fileName], workspace diff --git a/packages/react-scripts/config/jest/graphqlTransform.js b/packages/react-scripts/config/jest/graphqlTransform.js new file mode 100644 index 00000000000..5b70f07d6f2 --- /dev/null +++ b/packages/react-scripts/config/jest/graphqlTransform.js @@ -0,0 +1,18 @@ +// @remove-on-eject-begin +/** + * Copyright (c) 2018-present, Facebook, Inc. + * Copyright (c) 2016 Remind + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +// @remove-on-eject-end +'use strict'; + +const loader = require('graphql-tag/loader'); + +module.exports = { + process(src) { + return loader.call({ cacheable() {} }, src); + }, +}; diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 66f07baa283..398b8bf53b7 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,30 +278,10 @@ 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. + // The GraphQL loader preprocesses GraphQL queries in .graphql files. { - 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]', - }, - }, - ], + test: /\.(graphql)$/, + loader: 'graphql-tag/loader', }, // "file" loader makes sure those assets get served by WebpackDevServer. // When you `import` an asset, you get its (virtual) filename. diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 1d9a617c829..7b73dd787cd 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,30 +320,10 @@ 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. + // The GraphQL loader preprocesses GraphQL queries in .graphql files. { - 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]', - }, - }, - ], + test: /\.(graphql)$/, + loader: 'graphql-tag/loader', }, // "file" loader makes sure assets end up in the `build` folder. // When you `import` an asset, you get its filename. diff --git a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js index 06ec83602f3..bba497c49f3 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js @@ -31,6 +31,16 @@ describe('Integration', () => { ); }); + it('graphql files inclusion', async () => { + const doc = await initDOM('graphql-inclusion'); + const children = doc.getElementById('graphql-inclusion').children; + + // .graphql + expect(children[0].textContent.replace(/\s/g, '')).to.equal( + '{"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","variableDefinitions":[],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"test"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"test"},"value":{"kind":"StringValue","value":"test","block":false}}],"directives":[],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"test"},"arguments":[],"directives":[]}]}}]}}],"loc":{"start":0,"end":40,"source":{"body":"{\\ntest(test:\\"test\\"){\\ntest\\n}\\n}\\n","name":"GraphQLrequest","locationOffset":{"line":1,"column":1}}}}' + ); + }); + it('image inclusion', async () => { const doc = await initDOM('image-inclusion'); @@ -71,6 +81,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 d1affb48af9..c45ef2a38e8 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 => @@ -111,6 +111,11 @@ class App extends Component { this.setFeature(f.default) ); break; + case 'graphql-inclusion': + import('./features/webpack/GraphQLInclusion').then(f => + this.setFeature(f.default) + ); + break; case 'image-inclusion': import('./features/webpack/ImageInclusion').then(f => this.setFeature(f.default) @@ -174,6 +179,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/GraphQLInclusion.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/GraphQLInclusion.js new file mode 100644 index 00000000000..728b7a2847d --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/GraphQLInclusion.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import A from './assets/graphql.graphql'; + +export default () => ( +
+ {JSON.stringify(A)} +
+); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/GraphQLInclusion.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/GraphQLInclusion.test.js new file mode 100644 index 00000000000..914ce241bdd --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/GraphQLInclusion.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import GraphQLInclusion from './GraphQLInclusion'; + +describe('graphql files inclusion', () => { + it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(