From 18a2b57655a6be21a93949520a663825471fbd48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 9 Nov 2016 23:59:41 +0100 Subject: [PATCH] Add nuxt-build and nuxt-start, build:false and dev option --- README.md | 29 +++++++- bin/nuxt | 6 +- bin/nuxt-build | 27 ++++++++ bin/nuxt-dev | 26 ++++++++ bin/nuxt-start | 48 ++------------ examples/global-css/README.md | 11 +-- examples/global-css/package.json | 4 +- examples/plugins-vendor/README.md | 2 +- examples/with-ava/test/index.test.js | 12 ++-- lib/app/client.js | 2 +- lib/app/server.js | 4 +- lib/build/index.js | 92 +++++++------------------- lib/build/webpack/base.config.js | 67 ++++++++++++------- lib/build/webpack/client.config.js | 83 ++++++++++++----------- lib/build/webpack/server.config.js | 51 +++++++++----- lib/build/webpack/vue-loader.config.js | 43 ++++++++---- lib/nuxt.js | 10 +-- lib/server.js | 49 ++++++++++++++ lib/views/app.html | 2 +- test/index.js | 2 +- 20 files changed, 343 insertions(+), 227 deletions(-) create mode 100755 bin/nuxt-build create mode 100755 bin/nuxt-dev create mode 100644 lib/server.js diff --git a/README.md b/README.md index 553933f932..0372efb24a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ So far, we get: ## Using nuxt.js programmatically Nuxt is built on the top of ES2015, which makes the code more enjoyable and cleaner to read. It doesn't make use of any transpilers and depends upon Core V8 implemented features. -For these reasons, Nuxt.js targets Node.js `4.0` or higher (you might want to launch node with the `--harmony-proxies` flag if you running `node <= 6.5.0` ) +For these reasons, nuxt.js targets Node.js `4.0` or higher (you might want to launch node with the `--harmony-proxies` flag if you running `node <= 6.5.0` ) ```js const Nuxt = require('nuxt') @@ -115,3 +115,30 @@ cd node_modules/nuxt/ bin/nuxt examples/hello-world # Go to http://localhost:3000 ``` + +## Production deployment + +To deploy, instead of running next, you probably want to build ahead of time. Therefore, building and starting are separate commands: + +```bash +nuxt build +nuxt start +``` + +For example, to deploy with [`now`](https://zeit.co/now) a `package.json` like follows is recommended: +```json +{ + "name": "my-app", + "dependencies": { + "next": "latest" + }, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt start" + } +} +``` +Then run `now` and enjoy! + +Note: we recommend putting `.nuxt` in `.npmignore` or `.gitignore`. diff --git a/bin/nuxt b/bin/nuxt index df3cb08691..c12f2f3e38 100755 --- a/bin/nuxt +++ b/bin/nuxt @@ -3,10 +3,12 @@ const { join } = require('path') const { spawn } = require('cross-spawn') -const defaultCommand = 'start' +const defaultCommand = 'dev' const commands = new Set([ defaultCommand, - 'init' + 'init', + 'build', + 'start' ]) let cmd = process.argv[2] diff --git a/bin/nuxt-build b/bin/nuxt-build new file mode 100755 index 0000000000..b20a727b15 --- /dev/null +++ b/bin/nuxt-build @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +const fs = require('fs') +const Nuxt = require('../') +const { resolve } = require('path') + +const rootDir = resolve(process.argv.slice(2)[0] || '.') +const nuxtConfigFile = resolve(rootDir, 'nuxt.config.js') +let options = {} +if (fs.existsSync(nuxtConfigFile)) { + options = require(nuxtConfigFile) +} +if (typeof options.rootDir !== 'string') { + options.rootDir = rootDir +} + +options.dev = false // Create production build when calling `nuxt build` + +console.log('[nuxt] Building...') +new Nuxt(options) +.then((nuxt) => { + console.log('[nuxt] Building done') +}) +.catch((err) => { + console.error(err) + process.exit() +}) diff --git a/bin/nuxt-dev b/bin/nuxt-dev new file mode 100755 index 0000000000..069e62aaf0 --- /dev/null +++ b/bin/nuxt-dev @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +const fs = require('fs') +const Nuxt = require('../') +const Server = require('../lib/server') +const { resolve } = require('path') + +const rootDir = resolve(process.argv.slice(2)[0] || '.') +const nuxtConfigFile = resolve(rootDir, 'nuxt.config.js') +let options = {} +if (fs.existsSync(nuxtConfigFile)) { + options = require(nuxtConfigFile) +} +if (typeof options.rootDir !== 'string') { + options.rootDir = rootDir +} + +new Nuxt(options) +.then((nuxt) => { + new Server(nuxt) + .listen(process.env.PORT, process.env.HOST) +}) +.catch((err) => { + console.error(err) + process.exit() +}) diff --git a/bin/nuxt-start b/bin/nuxt-start index 5f4faa4ffe..1ef5bdd692 100755 --- a/bin/nuxt-start +++ b/bin/nuxt-start @@ -1,11 +1,8 @@ #!/usr/bin/env node -const http = require('http') -const co = require('co') const fs = require('fs') -const pify = require('pify') -const serveStatic = require('serve-static') const Nuxt = require('../') +const Server = require('../lib/server') const { resolve } = require('path') const rootDir = resolve(process.argv.slice(2)[0] || '.') @@ -18,6 +15,9 @@ if (typeof options.rootDir !== 'string') { options.rootDir = rootDir } +options.build = false // Disable building +options.dev = false // Force production mode (no webpack middlewares called) + new Nuxt(options) .then((nuxt) => { new Server(nuxt) @@ -27,43 +27,3 @@ new Nuxt(options) console.error(err) process.exit() }) - -class Server { - - constructor (nuxt) { - this.server = http.createServer(this.handle.bind(this)) - this.serveStatic = pify(serveStatic(resolve(rootDir, 'static'))) - this.nuxt = nuxt - return this - } - - handle (req, res) { - const method = req.method.toUpperCase() - const self = this - - if (method !== 'GET' && method !== 'HEAD') { - return this.nuxt.render(req, res) - } - co(function * () { - if (req.url.includes('/static/')) { - const url = req.url - req.url = req.url.replace('/static/', '/') - yield self.serveStatic(req, res) - req.url = url - } - }) - .then(() => { - // File not found - this.nuxt.render(req, res) - }) - } - - listen (port, host) { - host = host || 'localhost' - port = port || 3000 - this.server.listen(port, host, () => { - console.log('Ready on http://%s:%s', host, port) - }) - } - -} diff --git a/examples/global-css/README.md b/examples/global-css/README.md index a7d576b355..3302e91a94 100644 --- a/examples/global-css/README.md +++ b/examples/global-css/README.md @@ -27,19 +27,20 @@ module.exports = { To see the demo working: ```bash npm install -npm start +npm run dev ``` -Go to [http://localhost:8080](http://localhost:8080) and navigate inside the app. +Go to [http://localhost:3000](http://localhost:3000) and navigate inside the app. ## Production In production, they will be minified and extracted in a file named `styles.css` and added in the `` of the page. -To launch the demo in production mode so you can see the ``` populated with the `` tag: +To launch the demo in production mode so you can see the `` populated with the `` tag: ```bash -NODE_ENV=production npm start +npm run build +npm start ``` -Go to [http://localhost:8080](http://localhost:8080) and check the source code. +Go to [http://localhost:3000](http://localhost:3000) and check the source code. diff --git a/examples/global-css/package.json b/examples/global-css/package.json index 731fdcbd59..c451035856 100644 --- a/examples/global-css/package.json +++ b/examples/global-css/package.json @@ -9,6 +9,8 @@ "sass-loader": "^4.0.2" }, "scripts": { - "start": "nuxt" + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt start" } } diff --git a/examples/plugins-vendor/README.md b/examples/plugins-vendor/README.md index 567ed6516d..1dd6c7bcda 100644 --- a/examples/plugins-vendor/README.md +++ b/examples/plugins-vendor/README.md @@ -1,4 +1,4 @@ -# Using external modules and plugings with Nuxt.js +# Using external modules and plugings with nuxt.js ## Configuration: `build.vendor` diff --git a/examples/with-ava/test/index.test.js b/examples/with-ava/test/index.test.js index 95c11390c5..7869688653 100755 --- a/examples/with-ava/test/index.test.js +++ b/examples/with-ava/test/index.test.js @@ -11,10 +11,10 @@ let server = null // Init nuxt.js and create server listening on localhost:4000 test.before('Init nuxt.js', (t) => { - process.env.NODE_ENV = 'test' const Nuxt = require('../../../') const options = { - rootDir: resolve(__dirname, '..') + rootDir: resolve(__dirname, '..'), + dev: false } return new Nuxt(options) .then(function (_nuxt) { @@ -65,9 +65,11 @@ test('Route / exits and render HTML', async t => { */ test('Route / exits and render HTML', async t => { const window = await renderAndGetWindow('/') - t.is(window.document.querySelector('p').textContent, 'Hello world!') - t.is(window.document.querySelector('p').className, 'red-color') - t.true(window.document.querySelectorAll('style')[2].textContent.includes('.red-color {\n color: red;\n}')) + const element = window.document.querySelector('.red-color') + t.not(element, null) + t.is(element.textContent, 'Hello world!') + t.is(element.className, 'red-color') + t.is(window.getComputedStyle(element).color, 'red') }) // Close server and ask nuxt to stop listening to file changes diff --git a/lib/app/client.js b/lib/app/client.js index 9f7040b4cc..86ee09f892 100644 --- a/lib/app/client.js +++ b/lib/app/client.js @@ -176,5 +176,5 @@ Promise.all(resolveComponents) } }) .catch((err) => { - console.error('[Nuxt.js] Cannot load components', err) + console.error('[nuxt.js] Cannot load components', err) }) diff --git a/lib/app/server.js b/lib/app/server.js index 9b734e02cc..a5dd9aa7cd 100644 --- a/lib/app/server.js +++ b/lib/app/server.js @@ -4,7 +4,7 @@ import { pick } from 'lodash' import { app, router<%= (store ? ', store' : '') %> } from './index' import { getMatchedComponents, getContext } from './utils' -const isDev = process.env.NODE_ENV !== 'production' +const isDev = <%= isDev %> const _app = new Vue(app) // This exported function will be called by `bundleRenderer`. @@ -54,7 +54,7 @@ export default context => { })) .then((res) => { <% if (isDev) { %> - debug('Data fetch ' + context.req.url + ': ' + (Date.now() - s) + 'ms') + debug('Data fetching ' + context.req.url + ': ' + (Date.now() - s) + 'ms') <% } %> // datas are the first row of each context.nuxt.data = res.map((tab) => tab[0]) diff --git a/lib/build/index.js b/lib/build/index.js index 18f6e19f71..2e0813c0ba 100644 --- a/lib/build/index.js +++ b/lib/build/index.js @@ -42,21 +42,25 @@ const defaultsLoaders = [ ] module.exports = function * () { - if (this.options.build === false) { - return Promise.resolve() - } + const noBuild = this.options.build === false // Defaults build options if (this.options.build && Array.isArray(this.options.build.loaders)) { this.options.build = _.defaultsDeep(this.options.build, defaults) } else { this.options.build = _.defaultsDeep(this.options.build, defaults, { loaders: defaultsLoaders }) } + if (noBuild) { + const serverConfig = getWebpackServerConfig.call(this) + const bundlePath = join(serverConfig.output.path, serverConfig.output.filename) + createRenderer.call(this, fs.readFileSync(bundlePath, 'utf8')) + return Promise.resolve() + } /* ** Check if pages dir exists and warn if not */ if (!fs.existsSync(join(this.dir, 'pages'))) { if (fs.existsSync(join(this.dir, '..', 'pages'))) { - console.error('> No `pages` directory found. Did you mean to run `next` in the parent (`../`) directory?') + console.error('> No `pages` directory found. Did you mean to run `nuxt` in the parent (`../`) directory?') } else { console.error('> Couldn\'t find a `pages` directory. Please create one under the project root') } @@ -75,9 +79,11 @@ module.exports = function * () { /* ** Create .nuxt/, .nuxt/components and .nuxt/dist folders */ - yield del(r(this.dir, '.nuxt'), { force: process.env.NODE_ENV === 'test' }) + try { + yield del(r(this.dir, '.nuxt')) + } catch (e) {} yield mkdirp(r(this.dir, '.nuxt/components')) - if (this.isProd) { + if (!this.dev) { yield mkdirp(r(this.dir, '.nuxt/dist')) } /* @@ -116,24 +122,24 @@ module.exports = function * () { 'components/Loading.vue' ] let templateVars = { - isDev: this.isDev, + isDev: this.dev, store: this.options.store, css: this.options.css, plugins: this.options.plugins.map((p) => r(this.dir, p)), loading: (this.options.loading === 'string' ? r(this.dir, this.options.loading) : this.options.loading), components: { Loading: r(__dirname, '..', 'app', 'components', 'Loading.vue'), - ErrorPage: r(__dirname, '..', '..', 'pages', (this.isDev ? '_error-debug.vue' : '_error.vue')) + ErrorPage: r(__dirname, '..', '..', 'pages', (this.dev ? '_error-debug.vue' : '_error.vue')) }, routes: this.options.routes } if (this.options.store) { templateVars.storePath = r(this.dir, 'store') } - if (this.isDev && files.includes('pages/_error-debug.vue')) { + if (this.dev && files.includes('pages/_error-debug.vue')) { templateVars.components.ErrorPage = r(this.dir, 'pages/_error-debug.vue') } - if (!this.isDev && files.includes('pages/_error.vue')) { + if (!this.dev && files.includes('pages/_error.vue')) { templateVars.components.ErrorPage = r(this.dir, 'pages/_error.vue') } const readFile = pify(fs.readFile) @@ -151,7 +157,7 @@ module.exports = function * () { /* ** Generate .nuxt/dist/ files */ - if (this.isDev) { + if (this.dev) { debug('Adding webpack middlewares...') createWebpackMiddlewares.call(this) webpackWatchAndUpdate.call(this) @@ -164,66 +170,14 @@ module.exports = function * () { } } -function addGlobalWebpackConfig (config) { - const nodeModulesDir = join(__dirname, '..', '..', 'node_modules') - config.resolve = { - modules: [ - nodeModulesDir, - join(this.dir, 'node_modules') - ] - } - config.resolveLoader = { - modules: [ - nodeModulesDir, - join(this.dir, 'node_modules') - ] - } - config.module.rules = config.module.rules.concat(this.options.build.loaders) - return config -} - function getWebpackClientConfig () { - var config = require(r(__dirname, 'webpack', 'client.config.js')) - config = _.cloneDeep(config) - // Entry - config.entry.app = r(this.dir, '.nuxt', 'client.js') - // Add vendors - if (this.options.store) config.entry.vendor.push('vuex') - config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor) - // extract vendor chunks for better caching - config.plugins.push( - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - filename: this.options.build.filenames.vendor - }) - ) - // Output - config.output.path = r(this.dir, '.nuxt', 'dist') - config.output.filename = this.options.build.filenames.app - // Extract text plugin - if (this.isProd) { - const ExtractTextPlugin = require('extract-text-webpack-plugin') - let plugin = config.plugins.find((plugin) => plugin instanceof ExtractTextPlugin) - if (plugin) plugin.filename = this.options.build.filenames.css - } - return addGlobalWebpackConfig.call(this, config) + const clientConfigPath = r(__dirname, 'webpack', 'client.config.js') + return require(clientConfigPath).call(this) } function getWebpackServerConfig () { - var config = require(r(__dirname, 'webpack', 'server.config.js')) - config = _.cloneDeep(config) - // Entry - config.entry = r(this.dir, '.nuxt', 'server.js') - // Output - config.output.path = r(this.dir, '.nuxt', 'dist') - // Externals - config.externals = Object.keys(require(r(__dirname, '..', '..', 'package.json')).dependencies || {}) - const projectPackageJson = r(this.dir, 'package.json') - if (fs.existsSync(projectPackageJson)) { - config.externals = config.externals.concat(Object.keys(require(r(this.dir, 'package.json')).dependencies || {})) - } - config.externals = _.uniq(config.externals) - return addGlobalWebpackConfig.call(this, config) + const configServerPath = r(__dirname, 'webpack', 'server.config.js') + return require(configServerPath).call(this) } function createWebpackMiddlewares () { @@ -270,7 +224,7 @@ function webpackRunClient () { const serverCompiler = webpack(clientConfig) serverCompiler.run((err, stats) => { if (err) return reject(err) - console.log('[webpack:build:client]\n', stats.toString({ chunks: false, colors: true })) + console.log('[nuxt:build:client]\n', stats.toString({ chunks: false, colors: true })) resolve() }) }) @@ -282,7 +236,7 @@ function webpackRunServer () { const serverCompiler = webpack(serverConfig) serverCompiler.run((err, stats) => { if (err) return reject(err) - console.log('[webpack:build:server]\n', stats.toString({ chunks: false, colors: true })) + console.log('[nuxt:build:server]\n', stats.toString({ chunks: false, colors: true })) const bundlePath = join(serverConfig.output.path, serverConfig.output.filename) createRenderer.call(this, fs.readFileSync(bundlePath, 'utf8')) resolve() diff --git a/lib/build/webpack/base.config.js b/lib/build/webpack/base.config.js index 7c2c3a8c25..d10ff4a3f2 100644 --- a/lib/build/webpack/base.config.js +++ b/lib/build/webpack/base.config.js @@ -1,4 +1,5 @@ const vueLoaderConfig = require('./vue-loader.config') +const { join } = require('path') /* |-------------------------------------------------------------------------- @@ -8,29 +9,49 @@ const vueLoaderConfig = require('./vue-loader.config') | webpack config files |-------------------------------------------------------------------------- */ -module.exports = { - devtool: 'source-map', - entry: { - vendor: ['vue', 'vue-router', 'vue-meta', 'es6-promise', 'es6-object-assign'] - }, - output: { - publicPath: '/_nuxt/' - }, - module: { - rules: [ - { - test: /\.vue$/, - loader: 'vue', - options: vueLoaderConfig - }, - { - test: /\.js$/, - loader: 'babel', - exclude: /node_modules/, - options: { - presets: ['es2015', 'stage-2'] +module.exports = function () { + const nodeModulesDir = join(__dirname, '..', '..', '..', 'node_modules') + let config = { + devtool: 'source-map', + entry: { + vendor: ['vue', 'vue-router', 'vue-meta', 'es6-promise', 'es6-object-assign'] + }, + output: { + publicPath: '/_nuxt/' + }, + resolve: { + modules: [ + nodeModulesDir, + join(this.dir, 'node_modules') + ] + }, + resolveLoader: { + modules: [ + nodeModulesDir, + join(this.dir, 'node_modules') + ] + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue', + options: vueLoaderConfig.call(this) + }, + { + test: /\.js$/, + loader: 'babel', + exclude: /node_modules/, + options: { + presets: ['es2015', 'stage-2'] + } } - } - ] + ] + } } + // Add nuxt build loaders (can be configured in nuxt.config.js) + config.module.rules = config.module.rules.concat(this.options.build.loaders) + + // Return config + return config } diff --git a/lib/build/webpack/client.config.js b/lib/build/webpack/client.config.js index d5752dfacb..d8341d37d9 100644 --- a/lib/build/webpack/client.config.js +++ b/lib/build/webpack/client.config.js @@ -1,6 +1,7 @@ const webpack = require('webpack') +const ExtractTextPlugin = require('extract-text-webpack-plugin') const base = require('./base.config') -const vueConfig = require('./vue-loader.config') +const { resolve } = require('path') /* |-------------------------------------------------------------------------- @@ -12,47 +13,55 @@ const vueConfig = require('./vue-loader.config') | In production, will generate public/dist/style.css |-------------------------------------------------------------------------- */ +module.exports = function () { + let config = base.call(this) -const config = Object.assign({}, base, { - plugins: (base.plugins || []).concat([ + // Entry + config.entry.app = resolve(this.dir, '.nuxt', 'client.js') + + // Add vendors + if (this.options.store) { + config.entry.vendor.push('vuex') + } + config.entry.vendor = config.entry.vendor.concat(this.options.build.vendor) + + // Output + config.output.path = resolve(this.dir, '.nuxt', 'dist') + config.output.filename = this.options.build.filenames.app + + // Webpack plugins + config.plugins = (config.plugins || []).concat([ // strip comments in Vue code new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.NODE_ENV': JSON.stringify(this.dev ? 'development' : 'production'), 'process.BROWSER': true + }), + // Extract vendor chunks for better caching + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + filename: this.options.build.filenames.vendor }) ]) -}) -if (process.env.NODE_ENV === 'production') { - // Use ExtractTextPlugin to extract CSS into a single file - // so it's applied on initial render - const ExtractTextPlugin = require('extract-text-webpack-plugin') - - // vueConfig is already included in the config via LoaderOptionsPlugin - // here we overwrite the loader config for