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 = () => ( +