From e2bcb039cf1a4d9bb605655b0282026a5ab7ca41 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 5 Dec 2017 15:46:06 -0800 Subject: [PATCH] Add .jsx extension support (#3376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add .jsx extension * examples: add create-next-app (#3377) * examples: add create-next-app * fix with-typescript readme * Upgrading with-flow example to the latest flow-bin ver. 0.59.0 (#3337) For upgrading I used flow-upgrade module by https://yarnpkg.com/en/package/flow-upgrade * doc'd fs-routing option & added note on `passHref` (#3384) 2 changes: `passHref` - just added a cautionary note on the importance of `passHref`. We had a few days of no-href links on our site b/c we used a custom component instead of a raw `` tag, and Google bot wasn't crawling our links (confirmed in Google cache). Hurt our SEO a bit, so I thought it was worth noting. `useFileSystemPublicRoutes` - this is mentioned in https://github.com/zeit/next.js/pull/914 , but it doesn't appear any doc was actually added. We use `next-routes`, and we were serving all the files in `/pages/` in addition to their route patterns (ie duplicate content), which can be a pain w/ SEO and duplicate content. * fix typo in readme.md (#3385) * Upgrade styled-jsx to v2.2.1 (#3358) * Pulled encoding to top of head (#3214) * Remove next.d.ts to use @types/next (#3297) * Add with-mobx-state-tree example (#3179) * Adapt with-mobx example for with-mobx-state-tree * Remove unnecessary lastUpdate parameter to show off snapshot * update readme * make other.js more closely mimic index.js * Upgrade styled-jsx to v2.2.1 Includes some bug fixes. * Fix linting * Make sure import that doesn’t end in .jsx works * Move tests * Show error when .js and .jsx both exist * Remove .jsx when importing from ‘path.jsx’ * Fixes * Get .jsx resolver back * Revert "Get .jsx resolver back" This reverts commit 6f76712caa400e6f41a6a32ff80189a95b194cce. * Revert "Revert "Get .jsx resolver back"" This reverts commit 69e592e86e53f28d0e1f78009196b76f2f831866. * Add remove .jsx to preset * Remove jsx resolver * Revert "Remove jsx resolver" This reverts commit 5e3ef1aca134de47657d91485809cd801e13329f. * Revert "Revert "Remove jsx resolver"" This reverts commit 8248e5066cff1c7e33dac2e5a88ffe6856e3fc4e. * Revert "Revert "Revert "Remove jsx resolver""" This reverts commit 2a6d418a227ea4e59874b0374628ef497e527c52. * Make 1 component not use .jsx --- .../plugins/remove-dotjsx-from-import.js | 15 ++++++++ server/build/babel/preset.js | 1 + server/build/loaders/emit-file-loader.js | 19 ++++++++-- server/build/webpack.js | 37 ++++++++++++++----- server/config.js | 2 +- server/resolve.js | 4 ++ server/utils.js | 4 +- test/integration/basic/components/hello.jsx | 3 ++ test/integration/basic/components/world.jsx | 3 ++ .../basic/pages/custom-extension.jsx | 6 +++ test/integration/basic/test/rendering.js | 6 +++ 11 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 server/build/babel/plugins/remove-dotjsx-from-import.js create mode 100644 test/integration/basic/components/hello.jsx create mode 100644 test/integration/basic/components/world.jsx create mode 100644 test/integration/basic/pages/custom-extension.jsx diff --git a/server/build/babel/plugins/remove-dotjsx-from-import.js b/server/build/babel/plugins/remove-dotjsx-from-import.js new file mode 100644 index 00000000..b43b61ba --- /dev/null +++ b/server/build/babel/plugins/remove-dotjsx-from-import.js @@ -0,0 +1,15 @@ +// This plugins removes the `.jsx` extension from import statements. Because we transpile .jsx files to .js in .next +// E.g. `import Hello from '../components/hello.jsx'` will become `import Hello from '../components/hello'` +module.exports = function ({types}) { + return { + name: 'remove-dotjsx-from-import', + visitor: { + ImportDeclaration (path) { + const value = path.node.source.value + if (value.slice(-4) === '.jsx') { + path.node.source = types.stringLiteral(value.slice(0, -4)) + } + } + } + } +} diff --git a/server/build/babel/preset.js b/server/build/babel/preset.js index b2b5fac4..6ccebdc9 100644 --- a/server/build/babel/preset.js +++ b/server/build/babel/preset.js @@ -46,6 +46,7 @@ module.exports = (context, opts = {}) => ({ require.resolve('babel-preset-react') ], plugins: [ + require.resolve('./plugins/remove-dotjsx-from-import'), require.resolve('babel-plugin-react-require'), require.resolve('./plugins/handle-import'), require.resolve('babel-plugin-transform-object-rest-spread'), diff --git a/server/build/loaders/emit-file-loader.js b/server/build/loaders/emit-file-loader.js index 024dceb1..372112f9 100644 --- a/server/build/loaders/emit-file-loader.js +++ b/server/build/loaders/emit-file-loader.js @@ -2,17 +2,30 @@ import loaderUtils from 'loader-utils' module.exports = function (content, sourceMap) { this.cacheable() + const callback = this.async() + const resourcePath = this.resourcePath const query = loaderUtils.getOptions(this) + + // Allows you to do checks on the file name. For example it's used to check if there's both a .js and .jsx file. + if (query.validateFileName) { + try { + query.validateFileName(resourcePath) + } catch (err) { + callback(err) + return + } + } + const name = query.name || '[hash].[ext]' const context = query.context || this.options.context const regExp = query.regExp const opts = { context, content, regExp } - const interpolatedName = loaderUtils.interpolateName(this, name, opts) - + const interpolateName = query.interpolateName || ((name) => name) + const interpolatedName = interpolateName(loaderUtils.interpolateName(this, name, opts), {name, opts}) const emit = (code, map) => { this.emitFile(interpolatedName, code, map) - this.callback(null, code, map) + callback(null, code, map) } if (query.transform) { diff --git a/server/build/webpack.js b/server/build/webpack.js index 5c67aca6..69a62cd0 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -1,6 +1,6 @@ import { resolve, join, sep } from 'path' import { createHash } from 'crypto' -import { realpathSync } from 'fs' +import { realpathSync, existsSync } from 'fs' import webpack from 'webpack' import glob from 'glob-promise' import WriteFilePlugin from 'write-file-webpack-plugin' @@ -57,11 +57,11 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet // managing pages. if (dev) { for (const p of devPages) { - entries[join('bundles', p)] = [`./${p}?entry`] + entries[join('bundles', p.replace('.jsx', '.js'))] = [`./${p}?entry`] } } else { for (const p of pages) { - entries[join('bundles', p)] = [`./${p}?entry`] + entries[join('bundles', p.replace('.jsx', '.js'))] = [`./${p}?entry`] } } @@ -192,14 +192,14 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet } const rules = (dev ? [{ - test: /\.js(\?[^?]*)?$/, + test: /\.(js|jsx)(\?[^?]*)?$/, loader: 'hot-self-accept-loader', include: [ join(dir, 'pages'), nextPagesDir ] }, { - test: /\.js(\?[^?]*)?$/, + test: /\.(js|jsx)(\?[^?]*)?$/, loader: 'react-hot-loader/webpack', exclude: /node_modules/ }] : []) @@ -207,7 +207,7 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet test: /\.json$/, loader: 'json-loader' }, { - test: /\.(js|json)(\?[^?]*)?$/, + test: /\.(js|jsx|json)(\?[^?]*)?$/, loader: 'emit-file-loader', include: [dir, nextPagesDir], exclude (str) { @@ -215,17 +215,34 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet }, options: { name: 'dist/[path][name].[ext]', + // We need to strip off .jsx on the server. Otherwise require without .jsx doesn't work. + interpolateName: (name) => name.replace('.jsx', '.js'), + validateFileName (file) { + const cases = [{from: '.js', to: '.jsx'}, {from: '.jsx', to: '.js'}] + + for (const item of cases) { + const {from, to} = item + if (file.slice(-(from.length)) !== from) { + continue + } + + const filePath = file.slice(0, -(from.length)) + to + + if (existsSync(filePath)) { + throw new Error(`Both ${from} and ${to} file found. Please make sure you only have one of both.`) + } + } + }, // By default, our babel config does not transpile ES2015 module syntax because // webpack knows how to handle them. (That's how it can do tree-shaking) // But Node.js doesn't know how to handle them. So, we have to transpile them here. transform ({ content, sourceMap, interpolatedName }) { // Only handle .js files - if (!(/\.js$/.test(interpolatedName))) { + if (!(/\.(js|jsx)$/.test(interpolatedName))) { return { content, sourceMap } } const babelRuntimePath = require.resolve('babel-runtime/package').replace(/[\\/]package\.json$/, '') - const transpiled = babelCore.transform(content, { babelrc: false, sourceMaps: dev ? 'both' : false, @@ -235,6 +252,7 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet // That's why we need to do it here. // See more: https://github.com/zeit/next.js/issues/951 plugins: [ + require.resolve(join(__dirname, './babel/plugins/remove-dotjsx-from-import.js')), [require.resolve('babel-plugin-transform-es2015-modules-commonjs')], [ require.resolve('babel-plugin-module-resolver'), @@ -291,7 +309,7 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet presets: [require.resolve('./babel/preset')] } }, { - test: /\.js(\?[^?]*)?$/, + test: /\.(js|jsx)(\?[^?]*)?$/, loader: 'babel-loader', include: [dir], exclude (str) { @@ -321,6 +339,7 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet chunkFilename: '[name]' }, resolve: { + extensions: ['.js', '.jsx', '.json'], modules: [ nextNodeModulesDir, 'node_modules', diff --git a/server/config.js b/server/config.js index 7c61fdd3..10fcddd3 100644 --- a/server/config.js +++ b/server/config.js @@ -11,7 +11,7 @@ const defaultConfig = { assetPrefix: '', configOrigin: 'default', useFileSystemPublicRoutes: true, - pagesGlobPattern: 'pages/**/*.js' + pagesGlobPattern: 'pages/**/*.+(js|jsx)' } export default function getConfig (dir, customConfig) { diff --git a/server/resolve.js b/server/resolve.js index 2e6c2c89..543dded7 100644 --- a/server/resolve.js +++ b/server/resolve.js @@ -27,11 +27,13 @@ function getPaths (id) { const i = sep === '/' ? id : id.replace(/\//g, sep) if (i.slice(-3) === '.js') return [i] + if (i.slice(-4) === '.jsx') return [i] if (i.slice(-5) === '.json') return [i] if (i[i.length - 1] === sep) { return [ i + 'index.js', + i + 'index.jsx', i + 'index.json' ] } @@ -39,6 +41,8 @@ function getPaths (id) { return [ i + '.js', join(i, 'index.js'), + i + '.jsx', + join(i, 'index.jsx'), i + '.json', join(i, 'index.json') ] diff --git a/server/utils.js b/server/utils.js index 7bc32431..b7d3b760 100644 --- a/server/utils.js +++ b/server/utils.js @@ -1,8 +1,8 @@ import { join } from 'path' import { readdirSync, existsSync } from 'fs' -export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.js$/ -export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.js$/ +export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.(js|jsx)$/ +export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.(js|jsx)$/ export function getAvailableChunks (dir, dist) { const chunksDir = join(dir, dist, 'chunks') diff --git a/test/integration/basic/components/hello.jsx b/test/integration/basic/components/hello.jsx new file mode 100644 index 00000000..ca4066c6 --- /dev/null +++ b/test/integration/basic/components/hello.jsx @@ -0,0 +1,3 @@ +export const Hello = () => ( +
Hello
+ ) diff --git a/test/integration/basic/components/world.jsx b/test/integration/basic/components/world.jsx new file mode 100644 index 00000000..f9320963 --- /dev/null +++ b/test/integration/basic/components/world.jsx @@ -0,0 +1,3 @@ +export const World = () => ( +
World
+ ) diff --git a/test/integration/basic/pages/custom-extension.jsx b/test/integration/basic/pages/custom-extension.jsx new file mode 100644 index 00000000..13d7212b --- /dev/null +++ b/test/integration/basic/pages/custom-extension.jsx @@ -0,0 +1,6 @@ +import {World} from '../components/world' +import {Hello} from '../components/hello.jsx' + +export default () => ( +
+) diff --git a/test/integration/basic/test/rendering.js b/test/integration/basic/test/rendering.js index 47391034..603f24bc 100644 --- a/test/integration/basic/test/rendering.js +++ b/test/integration/basic/test/rendering.js @@ -38,6 +38,12 @@ export default function ({ app }, suiteName, render, fetch) { expect(html).not.toContain('') }) + it('should render the page with custom extension', async () => { + const html = await render('/custom-extension') + expect(html).toContain('
Hello
') + expect(html).toContain('
World
') + }) + test('renders styled jsx', async () => { const $ = await get$('/styled-jsx') const styleId = $('#blue-box').attr('class')