Skip to content

Commit

Permalink
Add support for emitting node modules from webpack
Browse files Browse the repository at this point in the history
This has two fold impact:

1. Allows for arbitrary loaders to run in both node and browser environments (Fixes vercel#1825)
2. Optimizes the build time for modules.

Risks:
- Transitions all harmony code to utilize harmony end to end. Previously this had not happened, which may break users in unexpected ways. (See vercel#2508)
- Require logic changed across the board to utilize node and webpack behaviors where possible, simplifying code, but potentially breaking things in the wild.
  • Loading branch information
kpdecker committed Oct 18, 2017
1 parent f7d2b96 commit 1f00e92
Show file tree
Hide file tree
Showing 18 changed files with 455 additions and 178 deletions.
2 changes: 0 additions & 2 deletions package.json
Expand Up @@ -51,11 +51,9 @@
"babel-core": "6.26.0",
"babel-generator": "6.26.0",
"babel-loader": "7.1.2",
"babel-plugin-module-resolver": "2.7.1",
"babel-plugin-react-require": "3.0.0",
"babel-plugin-syntax-dynamic-import": "6.18.0",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
"babel-plugin-transform-object-rest-spread": "6.26.0",
"babel-plugin-transform-react-jsx-source": "6.22.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.8",
Expand Down
20 changes: 1 addition & 19 deletions server/build/babel/preset.js
@@ -1,5 +1,3 @@
const relativeResolve = require('../root-module-relative-path').default(require)

// Resolve styled-jsx plugins
function styledJsxOptions (opts) {
if (!opts) {
Expand Down Expand Up @@ -51,22 +49,6 @@ module.exports = (context, opts = {}) => ({
require.resolve('babel-plugin-transform-class-properties'),
[require.resolve('babel-plugin-transform-runtime'), opts['transform-runtime'] || {}],
[require.resolve('styled-jsx/babel'), styledJsxOptions(opts['styled-jsx'])],
...plugins,
[
require.resolve('babel-plugin-module-resolver'),
{
alias: {
'babel-runtime': relativeResolve('babel-runtime/package'),
'next/link': relativeResolve('../../../lib/link'),
'next/prefetch': relativeResolve('../../../lib/prefetch'),
'next/css': relativeResolve('../../../lib/css'),
'next/dynamic': relativeResolve('../../../lib/dynamic'),
'next/head': relativeResolve('../../../lib/head'),
'next/document': relativeResolve('../../../server/document'),
'next/router': relativeResolve('../../../lib/router'),
'next/error': relativeResolve('../../../lib/error')
}
}
]
...plugins
]
})
24 changes: 0 additions & 24 deletions server/build/loaders/emit-file-loader.js

This file was deleted.

26 changes: 0 additions & 26 deletions server/build/root-module-relative-path.js

This file was deleted.

88 changes: 14 additions & 74 deletions server/build/webpack.js
Expand Up @@ -6,13 +6,13 @@ import WriteFilePlugin from 'write-file-webpack-plugin'
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
import UnlinkFilePlugin from './plugins/unlink-file-plugin'
import ServerSourcePlugin from '../../webpack/plugins/server-source-plugin'
import WebpackConfigPlugin from '../../webpack/plugins/webpack-config-plugin'
import PagesPlugin from './plugins/pages-plugin'
import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin'
import CombineAssetsPlugin from './plugins/combine-assets-plugin'
import getConfig from '../config'
import * as babelCore from 'babel-core'
import findBabelConfig from './babel/find-config'
import rootModuleRelativePath from './root-module-relative-path'

const documentPage = join('pages', '_document.js')
const defaultPages = [
Expand Down Expand Up @@ -127,6 +127,8 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production')
}),
new ServerSourcePlugin(),
new WebpackConfigPlugin(),
new PagesPlugin(),
new DynamicChunksPlugin(),
new CaseSensitivePathPlugin()
Expand Down Expand Up @@ -198,77 +200,6 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet
.concat([{
test: /\.json$/,
loader: 'json-loader'
}, {
test: /\.(js|json)(\?[^?]*)?$/,
loader: 'emit-file-loader',
include: [dir, nextPagesDir],
exclude (str) {
return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0
},
options: {
name: 'dist/[path][name].[ext]',
// 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))) {
return { content, sourceMap }
}

const transpiled = babelCore.transform(content, {
babelrc: false,
sourceMaps: dev ? 'both' : false,
// Here we need to resolve all modules to the absolute paths.
// Earlier we did it with the babel-preset.
// But since we don't transpile ES2015 in the preset this is not resolving.
// That's why we need to do it here.
// See more: https://github.com/zeit/next.js/issues/951
plugins: [
[require.resolve('babel-plugin-transform-es2015-modules-commonjs')],
[
require.resolve('babel-plugin-module-resolver'),
{
alias: {
'babel-runtime': relativeResolve('babel-runtime/package'),
'next/link': relativeResolve('../../lib/link'),
'next/prefetch': relativeResolve('../../lib/prefetch'),
'next/css': relativeResolve('../../lib/css'),
'next/dynamic': relativeResolve('../../lib/dynamic'),
'next/head': relativeResolve('../../lib/head'),
'next/document': relativeResolve('../../server/document'),
'next/router': relativeResolve('../../lib/router'),
'next/error': relativeResolve('../../lib/error'),
'styled-jsx/style': relativeResolve('styled-jsx/style')
}
}
]
],
inputSourceMap: sourceMap
})

// Strip ?entry to map back to filesystem and work with iTerm, etc.
let { map } = transpiled
let output = transpiled.code

if (map) {
let nodeMap = Object.assign({}, map)
nodeMap.sources = nodeMap.sources.map((source) => source.replace(/\?entry/, ''))
delete nodeMap.sourcesContent

// Output explicit inline source map that source-map-support can pickup via requireHook mode.
// Since these are not formal chunks, the devtool infrastructure in webpack does not output
// a source map for these files.
const sourceMapUrl = new Buffer(JSON.stringify(nodeMap), 'utf-8').toString('base64')
output = `${output}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${sourceMapUrl}`
}

return {
content: output,
sourceMap: transpiled.map
}
}
}
}, {
loader: 'babel-loader',
include: nextPagesDir,
Expand Down Expand Up @@ -327,7 +258,16 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet
},
plugins,
module: {
rules
rules,

// Avoid breaking libs such as core js Array.from and unfetch with webpack 3's
// harmony import behavior.
//
// Downstream users may want to disable this to further optimize their build
// output.
//
// See: https://github.com/webpack/webpack/issues/5111#issuecomment-309758504
strictThisContextOnImports: true
},
devtool: dev ? 'cheap-module-inline-source-map' : false,
performance: { hints: false }
Expand Down
23 changes: 22 additions & 1 deletion taskfile.js
Expand Up @@ -3,7 +3,7 @@ const childProcess = require('child_process')
const isWindows = /^win/.test(process.platform)

export async function compile (task) {
await task.parallel(['bin', 'server', 'lib', 'client'])
await task.parallel(['bin', 'server', 'webpack', 'lib', 'client'])
}

export async function bin (task, opts) {
Expand All @@ -21,6 +21,26 @@ export async function server (task, opts) {
notify('Compiled server files')
}

export async function webpack (task, opts) {
// We need to compile code that is used within webpack utilizing native classes
// to avoid issues with trying to extend webpack base classes with babel-classes
// that our lib, server, and client environments compile to (and should for
// compatibility with downstream consumers).
//
// https://github.com/babel/babel/issues/4269
await task.source(opts.src || 'webpack/**/*.js').babel({
presets: [
['env', {
targets: {
node: '5'
}
}]
],
babelrc: false
}).target('dist/webpack')
notify('Compiled webpack files')
}

export async function client (task, opts) {
await task.source(opts.src || 'client/**/*.js').babel().target('dist/client')
notify('Compiled client files')
Expand All @@ -39,6 +59,7 @@ export default async function (task) {
await task.watch('bin/*', 'bin')
await task.watch('pages/**/*.js', 'copy')
await task.watch('server/**/*.js', 'server')
await task.watch('webpack/**/*.js', 'webpack')
await task.watch('client/**/*.js', 'client')
await task.watch('lib/**/*.js', 'lib')
}
Expand Down
8 changes: 8 additions & 0 deletions webpack/buildin/node-config.js
@@ -0,0 +1,8 @@
// Webpack ApiPlugin aliases for node execution environment.
// https://github.com/webpack/webpack/blob/master/lib/APIPlugin.js
//
// Only used within the next server process and will be properly populated
// via the next runtime.

// __webpack_require__.p = the bundle public path
exports.publicPath = '/_next/webpack'
35 changes: 35 additions & 0 deletions webpack/dependencies/harmony-compatibility-template.js
@@ -0,0 +1,35 @@
export default class HarmonyCompatibilityDependencyTemplate {
apply (dep, source) {
source.insert(-10, `
const __webpack_exports__ = module.exports = exports = {};
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
const __webpack_require__ = require;
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// Via MainTemplate
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
// getDefaultExport function for compatibility with non-harmony modules");
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
`)
}
};
24 changes: 24 additions & 0 deletions webpack/dependencies/harmony-export-template.js
@@ -0,0 +1,24 @@
export default class HarmonyExportDependencyTemplate {
apply (dep, source) {
const used = dep.originModule.isUsed('default')
const content = this.getContent(dep.originModule, used)

if (dep.range) {
source.replace(dep.rangeStatement[0], dep.range[0] - 1, `${content}(`)
source.replace(dep.range[1], dep.rangeStatement[1] - 1, ');')
return
}

source.replace(dep.rangeStatement[0], dep.rangeStatement[1] - 1, content)
}

getContent (module, used) {
const exportsName = module.exportsArgument || 'exports'
if (used) {
return `/* harmony default export */ ${exportsName}[${JSON.stringify(
used
)}] = `
}
return '/* unused harmony default export */ var _unused_webpack_default_export = '
}
}
60 changes: 60 additions & 0 deletions webpack/dependencies/harmony-import-specifier-template.js
@@ -0,0 +1,60 @@
export default class HarmonyImportSpecifierDependencyTemplate {
apply (dep, source) {
const content = this.getContent(dep)
source.replace(dep.range[0], dep.range[1] - 1, content)
}

getContent (dep) {
const importedModule = dep.importDependency.module
const defaultImport =
dep.directImport &&
dep.id === 'default' &&
!(
importedModule &&
(!importedModule.meta || importedModule.meta.harmonyModule) &&
!/node_modules/.test(importedModule.resource)
)
const shortHandPrefix = this.getShortHandPrefix(dep)
const { importedVar } = dep
const importedVarSuffix = this.getImportVarSuffix(
dep,
defaultImport,
importedModule
)

if (dep.call && defaultImport) {
return `${shortHandPrefix}${importedVar}_default()`
}

if (dep.call && dep.id) {
return `${shortHandPrefix}Object(${importedVar}${importedVarSuffix})`
}

return `${shortHandPrefix}${importedVar}${importedVarSuffix}`
}

getImportVarSuffix (dep, defaultImport, importedModule) {
if (defaultImport) {
return '_default.a'
}

if (dep.id) {
const used =
importedModule && !/node_modules/.test(importedModule.resource)
? importedModule.isUsed(dep.id)
: dep.id
const optionalComment = dep.id !== used ? ` /* ${dep.id} */` : ''
return `[${JSON.stringify(used)}${optionalComment}]`
}

return ''
}

getShortHandPrefix (dep) {
if (!dep.shorthand) {
return ''
}

return `${dep.name}: `
}
}

0 comments on commit 1f00e92

Please sign in to comment.