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(, div); + }); +}); 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 0eb06a027e3..62bad3b1075 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 () => ; +export default () => ; 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 00000000000..edf34137940 --- /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 () =>
; 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 00000000000..f0c0bd68372 --- /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(, div); + }); +}); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/graphql.graphql b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/graphql.graphql new file mode 100644 index 00000000000..6125e344f55 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/graphql.graphql @@ -0,0 +1,5 @@ +{ + test(test: "test") { + test + } +} 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 00000000000..ad0ff93655a --- /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 9bb5a1a0758..d9e904d3ad9 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", @@ -46,6 +47,8 @@ "find-pkg": "1.0.0", "fs-extra": "5.0.0", "globby": "7.1.1", + "graphql": "0.12.3", + "graphql-tag": "2.6.1", "html-webpack-plugin": "2.30.1", "identity-obj-proxy": "3.0.0", "jest": "22.1.2", @@ -56,7 +59,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", diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 1546f4f4554..83e4bb630ed 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -95,7 +95,7 @@ checkBrowsers(paths.appPath) // Load proxy config const proxySetting = require(paths.appPackageJson).proxy; const proxyConfig = prepareProxy(proxySetting, paths.appPublic); - // Serve webpack assets generated by the compiler over a web sever. + // Serve webpack assets generated by the compiler over a web server. const serverConfig = createDevServerConfig( proxyConfig, urls.lanUrlForConfig diff --git a/packages/react-scripts/scripts/utils/createJestConfig.js b/packages/react-scripts/scripts/utils/createJestConfig.js index ea92a4f21ec..593d17090a7 100644 --- a/packages/react-scripts/scripts/utils/createJestConfig.js +++ b/packages/react-scripts/scripts/utils/createJestConfig.js @@ -38,7 +38,8 @@ module.exports = (resolve, rootDir, srcRoots) => { transform: { '^.+\\.(js|jsx|mjs)$': resolve('config/jest/babelTransform.js'), '^.+\\.css$': resolve('config/jest/cssTransform.js'), - '^(?!.*\\.(js|jsx|mjs|css|json)$)': resolve( + '^.+\\.(graphql)$': resolve('config/jest/graphqlTransform.js'), + '^(?!.*\\.(js|jsx|mjs|css|json|graphql)$)': resolve( 'config/jest/fileTransform.js' ), }, diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index b1ade09c396..2c533d49687 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -27,6 +27,7 @@ You can find the most recent version of this guide [here](https://github.com/fac - [Post-Processing CSS](#post-processing-css) - [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc) - [Adding Images, Fonts, and Files](#adding-images-fonts-and-files) +- [Adding GraphQL files](#adding-graphql-files) - [Using the `public` Folder](#using-the-public-folder) - [Changing the HTML](#changing-the-html) - [Adding Assets Outside of the Module System](#adding-assets-outside-of-the-module-system) @@ -729,6 +730,34 @@ Please be advised that this is also a custom feature of Webpack. **It is not required for React** but many people enjoy it (and React Native uses a similar mechanism for images).
An alternative way of handling static assets is described in the next section. +## Adding GraphQL files + +> Note: this feature is available with react-scripts@2.0.0 and higher. + +If you are using GraphQL, you can **`import` GraphQL files in a JavaScript module**. + +By importing GraphQL queries instead of using a [template tag](https://github.com/apollographql/graphql-tag), they are preprocessed at build time. This eliminates the need to process them on the client at run time. It also allows you to separate your GraphQL queries from your code. You can put a GraphQL query in a file with a `.graphql` extension. + +Here is an example: + +```js +// query.graphql +{ + githubStats(repository: "facebook/react") { + stars + } +} + +// foo.js + +import query from './query.graphql'; + +console.log(query); +// { +// "kind": "Document", +// ... +``` + ## Using the `public` Folder >Note: this feature is available with `react-scripts@0.5.0` and higher.