diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000..53a5ff72a2
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,27 @@
+module.exports = {
+ root: true,
+ parser: 'babel-eslint',
+ parserOptions: {
+ sourceType: 'module'
+ },
+ env: {
+ browser: true,
+ node: true,
+ mocha: true
+ },
+ extends: 'standard',
+ // required to lint *.vue files
+ plugins: [
+ 'html'
+ ],
+ // add your custom rules here
+ rules: {
+ // allow paren-less arrow functions
+ 'arrow-parens': 0,
+ // allow async-await
+ 'generator-star-spacing': 0,
+ // allow debugger during development
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
+ },
+ globals: {}
+}
diff --git a/.gitignore b/.gitignore
index a18a862098..1a70a5547f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,3 @@
-# build output
-dist
-
# dependencies
yarn.lock
node_modules
diff --git a/README.md b/README.md
index 4d29311df0..06ff887b95 100644
--- a/README.md
+++ b/README.md
@@ -47,5 +47,6 @@ So far, we get:
- Hot code reloading
- Server rendering and indexing of `./pages`
- Static file serving. `./static/` is mapped to `/static/`
+- Config file nuxt.config.js
To see how simple this is, check out the [sample app - nuxtgram](https://github.com/atinux/nuxtgram)
diff --git a/bin/nuxt b/bin/nuxt
new file mode 100755
index 0000000000..276b42ba89
--- /dev/null
+++ b/bin/nuxt
@@ -0,0 +1,29 @@
+#!/usr/bin/env node --harmony_proxies
+
+const { join } = require('path')
+const { spawn } = require('cross-spawn')
+
+const defaultCommand = 'start'
+const commands = new Set([
+ defaultCommand,
+ 'init'
+])
+
+let cmd = process.argv[2]
+let args
+
+if (commands.has(cmd)) {
+ args = process.argv.slice(3)
+} else {
+ cmd = defaultCommand
+ args = process.argv.slice(2)
+}
+
+const bin = join(__dirname, 'nuxt-' + cmd)
+
+const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] })
+proc.on('close', (code) => process.exit(code))
+proc.on('error', (err) => {
+ console.error(err)
+ process.exit(1)
+})
diff --git a/bin/nuxt-init b/bin/nuxt-init
new file mode 100755
index 0000000000..6331dcd080
--- /dev/null
+++ b/bin/nuxt-init
@@ -0,0 +1,81 @@
+#!/usr/bin/env node --harmony_proxies
+
+const co = require('co')
+const mkdirp = require('mkdirp-then')
+const pify = require('pify')
+const { resolve, join, basename } = require('path')
+const { existsSync, writeFile } = require('fs')
+
+const rootDir = resolve(process.argv.slice(2)[0] || '.')
+
+if (basename(rootDir) === 'pages') {
+ console.warn('Your root directory is named "pages". This looks suspicious. You probably want to go one directory up.')
+ process.exit(0)
+}
+
+co(function * () {
+ yield new Promise((resolve) => setTimeout(resolve, 0)) // avoid undefined variables basePackage, etc.
+ if (!existsSync(rootDir)) {
+ yield mkdirp(rootDir)
+ }
+ if (!existsSync(join(rootDir, 'package.json'))) {
+ yield pify(writeFile)(join(rootDir, 'package.json'), basePackage.replace(/my-app/g, basename(rootDir)))
+ }
+ if (!existsSync(join(rootDir, 'nuxt.config.js'))) {
+ yield pify(writeFile)(join(rootDir, 'nuxt.config.js'), baseConfig)
+ }
+ if (!existsSync(join(rootDir, 'static'))) {
+ yield mkdirp(join(rootDir, 'static'))
+ }
+ if (!existsSync(join(rootDir, 'pages'))) {
+ yield mkdirp(join(rootDir, 'pages'))
+ yield pify(writeFile)(join(rootDir, 'pages', 'index.vue'), basePage)
+ }
+})
+.then(() => {
+ console.log('Nuxt project [' + basename(rootDir) + '] created')
+})
+.catch((err) => {
+ console.error(err)
+ process.exit(1)
+})
+
+const basePackage = `{
+ "name": "my-app",
+ "description": "",
+ "dependencies": {
+ "nuxt": "latest"
+ },
+ "scripts": {
+ "start": "nuxt"
+ }
+}
+`
+
+const baseConfig = `module.exports = {
+ // Nuxt.js configuration file
+ // Please look at https://nuxtjs.org/docs/config-file
+}
+`
+
+const basePage = `
+
+ Hello {{ name }}! Hello World
+ Hello {{ name }}!This page has a title 🤔
+
+
+
diff --git a/examples/hello-world/pages/about.vue b/examples/hello-world/pages/about.vue
new file mode 100755
index 0000000000..36382aa404
--- /dev/null
+++ b/examples/hello-world/pages/about.vue
@@ -0,0 +1,3 @@
+
+ {{ title }}
+
Hello world!
')) + t.is(context.nuxt.error, null) + t.is(context.nuxt.data[0].name, 'world') +}) + +/* +** Example of testing via dom checking +*/ +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}')) +}) + +// Close server and ask nuxt to stop listening to file changes +test.after('Closing server and nuxt.js', t => { + server.close() + nuxt.stop() +}) diff --git a/index.js b/index.js new file mode 100644 index 0000000000..4f6b0c1b3c --- /dev/null +++ b/index.js @@ -0,0 +1,8 @@ +/*! + * nuxt.js + * MIT Licensed + */ + +'use strict' + +module.exports = require('./lib/nuxt') diff --git a/lib/app/App.vue b/lib/app/App.vue new file mode 100644 index 0000000000..c9649b0385 --- /dev/null +++ b/lib/app/App.vue @@ -0,0 +1,40 @@ + +LOADING...
+ + + + + diff --git a/lib/app/index.js b/lib/app/index.js new file mode 100644 index 0000000000..a280faca6b --- /dev/null +++ b/lib/app/index.js @@ -0,0 +1,24 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import router from './router' +<% if (store && storePath) { %>import store from '<%= storePath %>'<% } %> + +// import VueProgressBar from './plugins/vue-progressbar' +// Vue.use(VueProgressBar, { +// color: '#efc14e', +// failedColor: 'red', +// height: '2px' +// }) + +import App from './App.vue' +// create the app instance. +// here we inject the router and store to all child components, +// making them available everywhere as `this.$router` and `this.$store`. +const app = { + router, + <%= (store ? 'store,' : '') %> + ...App +} + +export { app, router<%= (store ? ', store' : '') %> } diff --git a/lib/app/router.js b/lib/app/router.js new file mode 100644 index 0000000000..208df1fc98 --- /dev/null +++ b/lib/app/router.js @@ -0,0 +1,38 @@ +import Vue from 'vue' +import Router from 'vue-router' +import Meta from 'vue-meta' + +Vue.use(Router) +Vue.use(Meta) + +<% routes.forEach(function (route) { %> +const <%= route._name %> = process.BROWSER ? () => System.import('<%= route._component %>') : require('<%= route._component %>') +<% }) %> + +const scrollBehavior = (to, from, savedPosition) => { + if (savedPosition) { + // savedPosition is only available for popstate navigations. + return savedPosition + } else { + // Scroll to the top by default + let position = { x: 0, y: 0 } + // if link has anchor, scroll to anchor by returning the selector + if (to.hash) { + position = { selector: to.hash } + } + return position + } +} + +export default new Router({ + mode: 'history', + scrollBehavior, + routes: [ + <% routes.forEach((route, i) => { %> + { + path: '<%= route.path %>', + component: <%= route._name %> + }<%= (i + 1 === routes.length ? '' : ',') %> + <% }) %> + ] +}) diff --git a/lib/app/server.js b/lib/app/server.js new file mode 100644 index 0000000000..9b734e02cc --- /dev/null +++ b/lib/app/server.js @@ -0,0 +1,70 @@ +const debug = require('debug')('nuxt:render') +import Vue from 'vue' +import { pick } from 'lodash' +import { app, router<%= (store ? ', store' : '') %> } from './index' +import { getMatchedComponents, getContext } from './utils' + +const isDev = process.env.NODE_ENV !== 'production' +const _app = new Vue(app) + +// This exported function will be called by `bundleRenderer`. +// This is where we perform data-prefetching to determine the +// state of our application before actually rendering it. +// Since data fetching is async, this function is expected to +// return a Promise that resolves to the app instance. +export default context => { + // set router's location + router.push(context.url) + + // Add route to the context + context.route = router.currentRoute + // Add meta infos + context.meta = _app.$meta() + // Add store to the context + <%= (store ? 'context.store = store' : '') %> + + // Nuxt object + context.nuxt = { data: [], error: null<%= (store ? ', state: null' : '') %> } + + <%= (isDev ? 'const s = isDev && Date.now()' : '') %> + // Call data & fecth hooks on components matched by the route. + let Components = getMatchedComponents(context.route) + if (!Components.length) { + context.nuxt.error = _app.error({ statusCode: 404, message: 'This page could not be found.', url: context.route.path }) + <%= (store ? 'context.nuxt.state = store.state' : '') %> + return Promise.resolve(_app) + } + return Promise.all(Components.map((Component) => { + let promises = [] + if (Component.data && typeof Component.data === 'function') { + Component._data = Component.data + var promise = Component.data(getContext(context)) + if (!(promise instanceof Promise)) promise = Promise.resolve(promise) + promise.then((data) => { + Component.data = () => data + }) + promises.push(promise) + } else { + promises.push(null) + } + if (Component.fetch) { + promises.push(Component.fetch(getContext(context))) + } + return Promise.all(promises) + })) + .then((res) => { + <% if (isDev) { %> + debug('Data fetch ' + context.req.url + ': ' + (Date.now() - s) + 'ms') + <% } %> + // datas are the first row of each + context.nuxt.data = res.map((tab) => tab[0]) + <%= (store ? '// Add the state from the vuex store' : '') %> + <%= (store ? 'context.nuxt.state = store.state' : '') %> + return _app + }) + .catch(function (error) { + context.nuxt.error = _app.error(error) + <%= (store ? 'context.nuxt.state = store.state' : '') %> + return _app + }) +} diff --git a/lib/app/utils.js b/lib/app/utils.js new file mode 100644 index 0000000000..b7e7f1c8da --- /dev/null +++ b/lib/app/utils.js @@ -0,0 +1,38 @@ +'use strict' + +export function getMatchedComponents (route) { + return [].concat.apply([], route.matched.map(function (m) { + return Object.keys(m.components).map(function (key) { + return m.components[key] + }) + })) +} + +export function flatMapComponents (route, fn) { + return Array.prototype.concat.apply([], route.matched.map(function (m, index) { + return Object.keys(m.components).map(function (key) { + return fn(m.components[key], m.instances[key], m, key, index) + }) + })) +} + +export function getContext (context) { + let ctx = { + isServer: !!context.isServer, + isClient: !!context.isClient, + <%= (store ? 'store: context.store,' : '') %> + route: (context.to ? context.to : context.route) + } + if (context.req) ctx.req = context.req + if (context.res) ctx.req = context.res + return ctx +} + +// Imported from vue-router +export function getLocation (base) { + var path = window.location.pathname + if (base && path.indexOf(base) === 0) { + path = path.slice(base.length) + } + return (path || '/') + window.location.search + window.location.hash +} diff --git a/lib/build/index.js b/lib/build/index.js new file mode 100644 index 0000000000..dcb5a98a30 --- /dev/null +++ b/lib/build/index.js @@ -0,0 +1,248 @@ +'use strict' + +const debug = require('debug')('nuxt:build') +const _ = require('lodash') +const del = require('del') +const fs = require('fs') +const glob = require('glob-promise') +const hash = require('hash-sum') +const mkdirp = require('mkdirp-then') +const pify = require('pify') +const webpack = require('webpack') +const { createBundleRenderer } = require('vue-server-renderer') +const { join, resolve } = require('path') +const r = resolve + +module.exports = function * () { + /* + ** 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?') + } else { + console.error('> Couldn\'t find a `pages` directory. Please create one under the project root') + } + process.exit() + } + if (this.options.store && !fs.existsSync(join(this.dir, 'store'))) { + console.error('> No `store` directory found (store option activated). Please create on under the project root') + process.exit() + } + if (this.options.store && !fs.existsSync(join(this.dir, 'store', 'index.js'))) { + console.error('> No `store/index.js` file found (store option activated). Please create the file.') + process.exit() + } + debug(`App root: ${this.dir}`) + debug('Generating .nuxt/ files...') + /* + ** Create .nuxt/, .nuxt/components and .nuxt/dist folders + */ + yield del(r(this.dir, '.nuxt'), { force: process.env.NODE_ENV === 'test' }) + yield mkdirp(r(this.dir, '.nuxt/components')) + if (this.isProd) { + yield mkdirp(r(this.dir, '.nuxt/dist')) + } + /* + ** Generate routes based on files + */ + const files = yield glob('pages/**/*.vue', { cwd: this.dir }) + let routes = [] + files.forEach((file) => { + let path = file.replace(/^pages/, '').replace(/index\.vue$/, '/').replace(/\.vue$/, '').replace(/\/{2,}/g, '/') + if (path[1] === '_') return + routes.push({ path: path, component: file }) + }) + this.options.routes.forEach((route) => { + route.component = r(this.dir, route.component) + }) + this.options.routes = routes.concat(this.options.routes) + // TODO: check .children + this.options.routes.forEach((route) => { + route._component = r(this.dir, route.component) + route._name = '_' + hash(route._component) + route.component = route._name + }) + /* + ** Interpret and move template files to .nuxt/ + */ + let templatesFiles = [ + 'App.vue', + 'client.js', + 'index.js', + 'router.js', + 'server.js', + 'utils.js', + 'components/Loading.vue' + ] + let templateVars = { + isDev: this.isDev, + store: this.options.store, + 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')) + }, + routes: this.options.routes + } + if (this.options.store) { + templateVars.storePath = r(this.dir, 'store') + } + if (this.isDev && files.includes('pages/_error-debug.vue')) { + templateVars.components.ErrorPage = r(this.dir, 'pages/_error-debug.vue') + } + if (!this.isDev && files.includes('pages/_error.vue')) { + templateVars.components.ErrorPage = r(this.dir, 'pages/_error.vue') + } + const readFile = pify(fs.readFile) + const writeFile = pify(fs.writeFile) + let moveTemplates = templatesFiles.map((file) => { + return readFile(r(__dirname, '..', 'app', file), 'utf8') + .then((fileContent) => { + const template = _.template(fileContent) + const content = template(templateVars) + return writeFile(r(this.dir, '.nuxt', file), content, 'utf8') + }) + }) + yield moveTemplates + debug('Files moved!') + /* + ** Generate .nuxt/dist/ files + */ + if (this.isDev) { + debug('Adding webpack middlewares...') + createWebpackMiddlewares.call(this) + webpackWatchAndUpdate.call(this) + } else { + debug('Building files...') + yield [ + webpackRunClient.call(this), + webpackRunServer.call(this) + ] + } + return this +} + +function getWebpackClientConfig () { + var config = require(r(__dirname, 'webpack', 'client.config.js')) + // 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.vendor) + // extract vendor chunks for better caching + config.plugins.push( + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + filename: this.options.filenames.vendor + }) + ) + // Output + config.output.path = r(this.dir, '.nuxt', 'dist') + config.output.filename = this.options.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.filenames.css + } + return config +} + +function getWebpackServerConfig () { + var config = require(r(__dirname, 'webpack', 'server.config.js')) + // 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 = [].concat(Object.keys(require(r(this.dir, 'package.json')).dependencies || {})) + } + config.externals = _.uniq(config.externals) + return config +} + +function createWebpackMiddlewares () { + const clientConfig = getWebpackClientConfig.call(this) + // setup on the fly compilation + hot-reload + clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] + clientConfig.plugins.push( + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin() + ) + const clientCompiler = webpack(clientConfig) + // Add the middlewares to the instance context + this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(clientCompiler, { + publicPath: clientConfig.output.publicPath, + stats: { + colors: true, + chunks: false + }, + quiet: true, + noInfo: true + })) + this.webpackHotMiddleware = pify(require('webpack-hot-middleware')(clientCompiler)) +} + +function webpackWatchAndUpdate () { + const MFS = require('memory-fs') // <- dependencies of webpack + const mfs = new MFS() + const serverConfig = getWebpackServerConfig.call(this) + const serverCompiler = webpack(serverConfig) + const outputPath = join(serverConfig.output.path, serverConfig.output.filename) + serverCompiler.outputFileSystem = mfs + this.webpackServerWatcher = serverCompiler.watch({}, (err, stats) => { + if (err) throw err + stats = stats.toJson() + stats.errors.forEach(err => console.error(err)) + stats.warnings.forEach(err => console.warn(err)) + createRenderer.call(this, mfs.readFileSync(outputPath, 'utf-8')) + }) +} + +function webpackRunClient () { + return new Promise((resolve, reject) => { + const clientConfig = getWebpackClientConfig.call(this) + const serverCompiler = webpack(clientConfig) + serverCompiler.run((err, stats) => { + if (err) return reject(err) + debug('[webpack:build:client]\n', stats.toString({ chunks: false, colors: true })) + resolve() + }) + }) +} + +function webpackRunServer () { + return new Promise((resolve, reject) => { + const serverConfig = getWebpackServerConfig.call(this) + const serverCompiler = webpack(serverConfig) + serverCompiler.run((err, stats) => { + if (err) return reject(err) + debug('[webpack: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() + }) + }) +} + +function createRenderer (bundle) { + process.env.VUE_ENV = (process.env.VUE_ENV ? process.env.VUE_ENV : 'server') + // Create bundle renderer to give a fresh context for every request + let cacheConfig = false + if (this.options.cache) { + this.options.cache = (typeof this.options.cache !== 'object' ? {} : this.options.cache) + cacheConfig = require('lru-cache')(_.defaults(this.options.cache, { + max: 1000, + maxAge: 1000 * 60 * 15 + })) + } + this.renderer = createBundleRenderer(bundle, { + cache: cacheConfig + }) + this.renderToString = pify(this.renderer.renderToString) + this.renderToStream = this.renderer.renderToStream +} diff --git a/lib/build/webpack/base.config.js b/lib/build/webpack/base.config.js new file mode 100644 index 0000000000..2b325ef200 --- /dev/null +++ b/lib/build/webpack/base.config.js @@ -0,0 +1,52 @@ +const vueLoaderConfig = require('./vue-loader.config') + +/* +|-------------------------------------------------------------------------- +| Webpack Shared Config +| +| This is the config which is extented by the server and client +| 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'] + } + }, + { + test: /\.(png|jpg|gif|svg)$/, + loader: 'url', + options: { + limit: 1000, // 1KO + name: 'img/[name].[ext]?[hash]' + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url', + query: { + limit: 1000, // 1 KO + name: 'fonts/[name].[hash:7].[ext]' + } + } + ] + } +} diff --git a/lib/build/webpack/client.config.js b/lib/build/webpack/client.config.js new file mode 100644 index 0000000000..6d7c426a5d --- /dev/null +++ b/lib/build/webpack/client.config.js @@ -0,0 +1,55 @@ +const webpack = require('webpack') +const base = require('./base.config') +const vueConfig = require('./vue-loader.config') + +/* +|-------------------------------------------------------------------------- +| Webpack Client Config +| +| Generate public/dist/client-vendor-bundle.js +| Generate public/dist/client-bundle.js +| +| In production, will generate public/dist/style.css +|-------------------------------------------------------------------------- +*/ + +const config = Object.assign({}, base, { + plugins: (base.plugins || []).concat([ + // strip comments in Vue code + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.BROWSER': true + }) + ]) +}) + +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 diff --git a/pages/_error.vue b/pages/_error.vue new file mode 100644 index 0000000000..889021e0db --- /dev/null +++ b/pages/_error.vue @@ -0,0 +1,60 @@ + +{{ name }}
+ + + diff --git a/test/fixtures/basic/pages/css.vue b/test/fixtures/basic/pages/css.vue new file mode 100755 index 0000000000..902c7a07f2 --- /dev/null +++ b/test/fixtures/basic/pages/css.vue @@ -0,0 +1,9 @@ + +The answer is {{ answer }}
+The answer is 42
Kobe Bryant
')) +}) + +function render (url, ctx) { + return _render(url, ctx, { dir, staticMarkup: true }) +}