diff --git a/examples/with-dynamic-import/README.md b/examples/with-dynamic-import/README.md new file mode 100644 index 00000000..79f242e4 --- /dev/null +++ b/examples/with-dynamic-import/README.md @@ -0,0 +1,27 @@ +# Example app with dynamic-imports + +## How to use + +Download the example [or clone the repo](https://github.com/zeit/next.js): + +```bash +curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/shared-modules +cd shared-modules +``` + +Install it and run: + +```bash +npm install +npm run dev +``` + +Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) + +```bash +now +``` + +## The idea behind the example + +This examples shows how to dynamically import modules via [`import()`](https://github.com/tc39/proposal-dynamic-import) API diff --git a/examples/with-dynamic-import/components/Counter.js b/examples/with-dynamic-import/components/Counter.js new file mode 100644 index 00000000..959d8ae9 --- /dev/null +++ b/examples/with-dynamic-import/components/Counter.js @@ -0,0 +1,19 @@ +import React from 'react' + +let count = 0 + +export default class Counter extends React.Component { + add () { + count += 1 + this.forceUpdate() + } + + render () { + return ( +
+

Count is: {count}

+ +
+ ) + } +} diff --git a/examples/with-dynamic-import/components/Header.js b/examples/with-dynamic-import/components/Header.js new file mode 100644 index 00000000..4409ca63 --- /dev/null +++ b/examples/with-dynamic-import/components/Header.js @@ -0,0 +1,19 @@ +import Link from 'next/link' + +export default () => ( +
+ + Home + + + + About + +
+) + +const styles = { + a: { + marginRight: 10 + } +} diff --git a/examples/with-dynamic-import/components/hello.js b/examples/with-dynamic-import/components/hello.js new file mode 100644 index 00000000..b2441590 --- /dev/null +++ b/examples/with-dynamic-import/components/hello.js @@ -0,0 +1,3 @@ +export default () => ( +

Hello World (imported dynamiclly)

+) diff --git a/examples/with-dynamic-import/lib/with-import.js b/examples/with-dynamic-import/lib/with-import.js new file mode 100644 index 00000000..ea537f48 --- /dev/null +++ b/examples/with-dynamic-import/lib/with-import.js @@ -0,0 +1,23 @@ +import React from 'react' + +export default function withImport (promise, Loading = () => (

Loading...

)) { + return class Comp extends React.Component { + constructor (...args) { + super(...args) + this.state = { AsyncComponent: null } + } + + componentDidMount () { + promise.then((AsyncComponent) => { + this.setState({ AsyncComponent }) + }) + } + + render () { + const { AsyncComponent } = this.state + if (!AsyncComponent) return () + + return + } + } +} diff --git a/examples/with-dynamic-import/package.json b/examples/with-dynamic-import/package.json new file mode 100644 index 00000000..21309efd --- /dev/null +++ b/examples/with-dynamic-import/package.json @@ -0,0 +1,18 @@ +{ + "name": "with-dynamic-import", + "version": "1.0.0", + "description": "This example features:", + "main": "index.js", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "*", + "react": "^15.4.2", + "react-dom": "^15.4.2" + }, + "author": "", + "license": "ISC" +} diff --git a/examples/with-dynamic-import/pages/about.js b/examples/with-dynamic-import/pages/about.js new file mode 100644 index 00000000..6468d744 --- /dev/null +++ b/examples/with-dynamic-import/pages/about.js @@ -0,0 +1,12 @@ +import Header from '../components/Header' +import Counter from '../components/Counter' + +const About = () => ( +
+
+

This is the about page.

+ +
+) + +export default About diff --git a/examples/with-dynamic-import/pages/index.js b/examples/with-dynamic-import/pages/index.js new file mode 100644 index 00000000..ac9508ae --- /dev/null +++ b/examples/with-dynamic-import/pages/index.js @@ -0,0 +1,15 @@ +import React from 'react' +import Header from '../components/Header' +import Counter from '../components/Counter' +import withImport from '../lib/with-import' + +const DynamicComponent = withImport(import('../components/hello')) + +export default () => ( +
+
+ +

HOME PAGE is here!

+ +
+) diff --git a/package.json b/package.json index 309f9557..013a3918 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "babel-loader": "6.4.1", "babel-plugin-module-resolver": "2.6.2", "babel-plugin-react-require": "3.0.0", + "babel-plugin-syntax-dynamic-import": "6.18.0", "babel-plugin-transform-class-properties": "6.22.0", "babel-plugin-transform-es2015-modules-commonjs": "6.24.0", "babel-plugin-transform-object-rest-spread": "6.22.0", @@ -61,6 +62,7 @@ "babel-preset-latest": "6.24.0", "babel-preset-react": "6.23.0", "babel-runtime": "6.23.0", + "babel-template": "6.24.1", "case-sensitive-paths-webpack-plugin": "2.0.0", "cross-spawn": "5.1.0", "del": "2.2.2", diff --git a/server/build/babel/plugins/handle-import.js b/server/build/babel/plugins/handle-import.js new file mode 100644 index 00000000..5d1a5242 --- /dev/null +++ b/server/build/babel/plugins/handle-import.js @@ -0,0 +1,41 @@ +// Based on https://github.com/airbnb/babel-plugin-dynamic-import-webpack +// We've added support for SSR with this version +import template from 'babel-template' +import syntax from 'babel-plugin-syntax-dynamic-import' +import UUID from 'uuid' + +const TYPE_IMPORT = 'Import' + +const buildImport = (args) => (template(` + ( + new Promise((resolve) => { + if (process.pid) { + eval('require.ensure = (deps, callback) => (callback(require))') + } + + require.ensure([], (require) => { + let m = require(SOURCE) + m = m.default || m + m.__webpackChunkName = '${args.name}.js' + resolve(m); + }, 'chunks/${args.name}.js'); + }) + ) +`)) + +export default () => ({ + inherits: syntax, + + visitor: { + CallExpression (path) { + if (path.node.callee.type === TYPE_IMPORT) { + const newImport = buildImport({ + name: UUID.v4() + })({ + SOURCE: path.node.arguments + }) + path.replaceWith(newImport) + } + } + } +}) diff --git a/server/build/babel/preset.js b/server/build/babel/preset.js index 8a61e22b..5a1cf53b 100644 --- a/server/build/babel/preset.js +++ b/server/build/babel/preset.js @@ -20,6 +20,7 @@ module.exports = { ], plugins: [ require.resolve('babel-plugin-react-require'), + require.resolve('./plugins/handle-import'), require.resolve('babel-plugin-transform-object-rest-spread'), require.resolve('babel-plugin-transform-class-properties'), require.resolve('babel-plugin-transform-runtime'), diff --git a/server/build/webpack.js b/server/build/webpack.js index d32360c8..462c592a 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -277,7 +277,9 @@ export default async function createCompiler (dir, { dev = false, quiet = false, // append hash id for cache busting return `webpack:///${resourcePath}?${id}` - } + }, + // This saves chunks with the name given via require.ensure() + chunkFilename: '[name]' }, resolve: { modules: [ diff --git a/server/index.js b/server/index.js index d6af168e..6078d962 100644 --- a/server/index.js +++ b/server/index.js @@ -91,6 +91,13 @@ export default class Server { await this.serveStatic(req, res, p) }, + // This is to support, webpack dynamic imports in production. + '/_webpack/chunks/:name': async (req, res, params) => { + res.setHeader('Cache-Control', 'max-age=365000000, immutable') + const p = join(this.dir, '.next', 'chunks', params.name) + await this.serveStatic(req, res, p) + }, + '/_next/:hash/manifest.js': async (req, res, params) => { this.handleBuildHash('manifest.js', params.hash, res) const p = join(this.dir, `${this.dist}/manifest.js`) diff --git a/yarn.lock b/yarn.lock index 7b397e2c..de75ba66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -504,6 +504,10 @@ babel-plugin-syntax-class-properties@^6.8.0: version "6.13.0" resolved "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + babel-plugin-syntax-exponentiation-operator@^6.8.0: version "6.13.0" resolved "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" @@ -894,6 +898,16 @@ babel-template@^6.16.0, babel-template@^6.22.0, babel-template@^6.23.0, babel-te babylon "^6.11.0" lodash "^4.2.0" +babel-template@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + babylon "^6.11.0" + lodash "^4.2.0" + babel-traverse@6.21.0: version "6.21.0" resolved "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.21.0.tgz#69c6365804f1a4f69eb1213f85b00a818b8c21ad" @@ -922,6 +936,20 @@ babel-traverse@^6.18.0, babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-tr invariant "^2.2.0" lodash "^4.2.0" +babel-traverse@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" + dependencies: + babel-code-frame "^6.22.0" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + babylon "^6.15.0" + debug "^2.2.0" + globals "^9.0.0" + invariant "^2.2.0" + lodash "^4.2.0" + babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.21.0, babel-types@^6.22.0, babel-types@^6.23.0: version "6.23.0" resolved "https://registry.npmjs.org/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf" @@ -931,6 +959,15 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.21.0, babel-types@^6.22 lodash "^4.2.0" to-fast-properties "^1.0.1" +babel-types@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" + dependencies: + babel-runtime "^6.22.0" + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^1.0.1" + babylon@6.14.1, babylon@^6.11.0: version "6.14.1" resolved "https://registry.npmjs.org/babylon/-/babylon-6.14.1.tgz#956275fab72753ad9b3435d7afe58f8bf0a29815" @@ -4023,11 +4060,11 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" -punycode@1.3.2: +punycode@1.3.2, punycode@^1.2.4: version "1.3.2" resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" -punycode@^1.2.4, punycode@^1.4.1: +punycode@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"