diff --git a/.gitignore b/.gitignore index baac52a82b..3395ea12b6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ coverage *.lcov .nyc_output .vscode + +# Intellij idea +*.iml +.idea diff --git a/bin/nuxt-build b/bin/nuxt-build index 50dbc246b8..c85d1b6fed 100755 --- a/bin/nuxt-build +++ b/bin/nuxt-build @@ -52,12 +52,13 @@ if (analyzeBuild) { } console.log('[nuxt] Building...') // eslint-disable-line no-console -var nuxt = new Nuxt(options) -nuxt.build() -.then(() => { - console.log('[nuxt] Building done') // eslint-disable-line no-console -}) -.catch((err) => { - console.error(err) // eslint-disable-line no-console - process.exit(1) +new Nuxt(options).then(nuxt => { + nuxt.build() + .then(() => { + console.log('[nuxt] Building done') // eslint-disable-line no-console + }) + .catch((err) => { + console.error(err) // eslint-disable-line no-console + process.exit(1) + }) }) diff --git a/bin/nuxt-dev b/bin/nuxt-dev index 0c2c1157a9..351a82a680 100755 --- a/bin/nuxt-dev +++ b/bin/nuxt-dev @@ -39,15 +39,16 @@ if (typeof options.rootDir !== 'string') { } options.dev = true // Add hot reloading and watching changes -var nuxt = new Nuxt(options) -var server = new nuxt.Server(nuxt) -.listen(process.env.PORT || process.env.npm_package_config_nuxt_port, process.env.HOST || process.env.npm_package_config_nuxt_host) -listenOnConfigChanges(nuxt, server) +new Nuxt(options).then(nuxt => { + var server = new nuxt.Server(nuxt) + .listen(process.env.PORT || process.env.npm_package_config_nuxt_port, process.env.HOST || process.env.npm_package_config_nuxt_host) + listenOnConfigChanges(nuxt, server) -nuxt.build() -.catch((err) => { - console.error(err) // eslint-disable-line no-console - process.exit(1) + nuxt.build() + .catch((err) => { + console.error(err) // eslint-disable-line no-console + process.exit(1) + }) }) function listenOnConfigChanges (nuxt, server) { @@ -68,7 +69,7 @@ function listenOnConfigChanges (nuxt, server) { .then(() => { nuxt.renderer = null debug('Rebuilding the app...') - return new Nuxt(options).build() + return (new Nuxt(options)).then(nuxt => nuxt.build()) }) .then((nuxt) => { server.nuxt = nuxt diff --git a/bin/nuxt-generate b/bin/nuxt-generate index 10b1c4ff70..8d085b666c 100755 --- a/bin/nuxt-generate +++ b/bin/nuxt-generate @@ -20,12 +20,13 @@ if (typeof options.rootDir !== 'string') { options.dev = false // Force production mode (no webpack middleware called) console.log('[nuxt] Generating...') // eslint-disable-line no-console -var nuxt = new Nuxt(options) -nuxt.generate() -.then(() => { - console.log('[nuxt] Generate done') // eslint-disable-line no-console -}) -.catch((err) => { - console.error(err) // eslint-disable-line no-console - process.exit(1) +new Nuxt(options).then(nuxt => { + nuxt.generate() + .then(() => { + console.log('[nuxt] Generate done') // eslint-disable-line no-console + }) + .catch((err) => { + console.error(err) // eslint-disable-line no-console + process.exit(1) + }) }) diff --git a/bin/nuxt-start b/bin/nuxt-start index 9190178576..f39daf9b19 100755 --- a/bin/nuxt-start +++ b/bin/nuxt-start @@ -16,10 +16,10 @@ if (typeof options.rootDir !== 'string') { } options.dev = false // Force production mode (no webpack middleware called) -var nuxt = new Nuxt(options) - -new nuxt.Server(nuxt) -.listen( - process.env.PORT || process.env.npm_package_config_nuxt_port, - process.env.HOST || process.env.npm_package_config_nuxt_host -) +new Nuxt(options).then(nuxt => { + new nuxt.Server(nuxt) + .listen( + process.env.PORT || process.env.npm_package_config_nuxt_port, + process.env.HOST || process.env.npm_package_config_nuxt_host + ) +}) diff --git a/lib/app/store.js b/lib/app/store.js index b0679c84af..54b68a18c4 100644 --- a/lib/app/store.js +++ b/lib/app/store.js @@ -1,15 +1,59 @@ import Vue from 'vue' import Vuex from 'vuex' + Vue.use(Vuex) -let files = require.context('~/store', true, /^\.\/.*\.(js|ts)$/) -let filenames = files.keys() +// Recursive find files in ~/store +const files = require.context('~/store', true, /^\.\/.*\.(js|ts)$/) +const filenames = files.keys() +// Store +let storeData = {} + +// Check if store/index.js exists +if (filenames.indexOf('./index.js') !== -1) { + storeData = getModule('./index.js') +} + +// Store modules +if (!storeData.modules) { + storeData.modules = {} +} + +for (let filename of filenames) { + let name = filename.replace(/^\.\//, '').replace(/\.(js|ts)$/, '') + if (name === 'index') continue + + let namePath = name.split(/\//) + let module = getModuleNamespace(storeData, namePath) + + name = namePath.pop() + module[name] = getModule(filename) + module[name].namespaced = true +} + +// createStore +export const createStore = storeData instanceof Function ? storeData : () => { + return new Vuex.Store(Object.assign({}, storeData, { + state: storeData.state instanceof Function ? storeData.state() : {} + })) +} + +// Dynamically require module function getModule (filename) { - let file = files(filename) - return file.default - ? file.default - : file + const file = files(filename) + const module = file.default || file + if (module.state && typeof module.state !== 'function') { + // eslint-disable-next-line no-console + console.error('[nuxt] store state should be a function.') + return + } + if (module.commit) { + // eslint-disable-next-line no-console + console.error('[nuxt] store should export raw store options instead of an instance.') + return + } + return module } function getModuleNamespace (storeData, namePath) { @@ -22,43 +66,3 @@ function getModuleNamespace (storeData, namePath) { storeData.modules[namespace].modules = storeData.modules[namespace].modules || {} return getModuleNamespace(storeData.modules[namespace], namePath) } - -let store -let storeData = {} - -// Check if store/index.js returns a vuex store -if (filenames.indexOf('./index.js') !== -1) { - let mainModule = getModule('./index.js') - if (mainModule.commit) { - console.error('[nuxt.js] store/index should export raw store options instead of an instance.') - } else { - if (mainModule.state && typeof mainModule.state !== 'function') { - console.error('[nuxt.js] store state should be a function.') - } - storeData = mainModule - } -} - -// Generate the store if there is no store yet -if (store == null) { - storeData.modules = storeData.modules || {} - for (let filename of filenames) { - let name = filename.replace(/^\.\//, '').replace(/\.(js|ts)$/, '') - if (name === 'index') continue - - let namePath = name.split(/\//) - let module = getModuleNamespace(storeData, namePath) - - name = namePath.pop() - module[name] = getModule(filename) - module[name].namespaced = true - - if (typeof module[name].state !== 'function') { - console.error('[nuxt.js] store module state should be a function.') - } - } -} - -export function createStore () { - return new Vuex.Store(storeData) -} diff --git a/lib/build.js b/lib/build.js index 66f38e39b1..f683af0fa6 100644 --- a/lib/build.js +++ b/lib/build.js @@ -57,7 +57,9 @@ const defaults = { loaders: [], plugins: [], babel: {}, - postcss: [] + postcss: [], + templates: [], + watch: [] } const defaultsLoaders = [ { @@ -189,6 +191,7 @@ function * generateRoutesAndFiles () { ] this.options.store = fs.existsSync(join(this.srcDir, 'store')) let templateVars = { + nuxt: this.options, uniqBy: _.uniqBy, isDev: this.dev, router: { @@ -221,7 +224,7 @@ function * generateRoutesAndFiles () { templateVars.router.routes = createRoutes(files, this.srcDir) if (typeof this.options.router.extendRoutes === 'function') { // let the user extend the routes - this.options.router.extendRoutes(templateVars.router.routes, r) + this.options.router.extendRoutes.call(this, templateVars.router.routes, r) } // Routes for Generate command this.routes = flatRoutes(templateVars.router.routes) @@ -239,8 +242,14 @@ function * generateRoutesAndFiles () { if (this.options.store) { templatesFiles.push('store.js') } - let moveTemplates = templatesFiles.map((file) => { - return readFile(r(__dirname, 'app', file), 'utf8') + // Resolve all internal template files relative to app directory + templatesFiles = templatesFiles.map(file => { return {src: r(__dirname, 'app', file), dst: file} }) + // Add external template files (used in modules) + if (Array.isArray(this.options.build.templates)) { + templatesFiles = templatesFiles.concat(this.options.build.templates) + } + let moveTemplates = templatesFiles.map(({src, dst, options}) => { + return readFile(src, 'utf8') .then((fileContent) => { const template = _.template(fileContent, { imports: { @@ -248,8 +257,10 @@ function * generateRoutesAndFiles () { hash } }) - const content = template(templateVars) - const path = r(this.dir, '.nuxt', file) + const content = template(Object.assign({}, templateVars, { + options: options || {} + })) + const path = r(this.dir, '.nuxt', dst) return writeFile(path, content, 'utf8') .then(() => { // Fix webpack loop (https://github.com/webpack/watchpack/issues/25#issuecomment-287789288) @@ -510,7 +521,11 @@ function watchPages () { const refreshFiles = _.debounce(() => { co(generateRoutesAndFiles.bind(this)) }, 200) + // Watch for internals this.pagesFilesWatcher = chokidar.watch(patterns, options) .on('add', refreshFiles) .on('unlink', refreshFiles) + // Watch for custom provided files + this.customFilesWatcher = chokidar.watch(_.uniq(this.options.build.watch), options) + .on('change', refreshFiles) } diff --git a/lib/module.js b/lib/module.js new file mode 100755 index 0000000000..4aa812b128 --- /dev/null +++ b/lib/module.js @@ -0,0 +1,119 @@ +'use strict' + +import path from 'path' +import fs from 'fs' +import {uniq} from 'lodash' +import hash from 'hash-sum' +import {chainFn} from './utils' + +class Module { + constructor (nuxt) { + this.nuxt = nuxt + this.options = nuxt.options + } + + addVendor (vendor) { + if (!vendor) { + return + } + this.options.build.vendor = uniq(this.options.build.vendor.concat(vendor)) + } + + addTemplate (template) { + if (!template) { + return + } + // Validate & parse source + const src = template.src || template + const srcPath = path.parse(src) + if (!src || typeof src !== 'string' || !fs.existsSync(src)) { + // eslint-disable-next-line no-console + console.warn('[Nuxt] invalid template', template) + return + } + // Generate unique and human readable dst filename + const dst = template.fileName || + (path.basename(srcPath.dir) + '.' + srcPath.name + '.' + hash(src) + '.' + srcPath.ext) + // Add to templates list + const templateObj = { + src, + dst, + options: template.options + } + this.options.build.templates.push(templateObj) + // Watch template for changes + this.addWatch(src) + return templateObj + } + + addWatch (pattern) { + this.options.build.watch.push(pattern) + } + + addPlugin (template) { + const {dst} = this.addTemplate(template) + // Add to nuxt plugins + this.options.plugins.push({ + src: '~/.nuxt/' + dst, + ssr: Boolean(template.ssr) + }) + } + + addServerMiddleware (middleware) { + this.options.serverMiddlewares.push(middleware) + } + + extendBuild (fn) { + this.options.build.extend = chainFn(this.options.build.extend, fn) + } + + extendRoutes (fn) { + this.options.router.extendRoutes = chainFn(this.options.router.extendRoutes, fn) + } + + installModule (moduleOpts) { + if (!moduleOpts) { + return + } + // Allows passing runtime options to each module + const options = moduleOpts.options || {} + let src = moduleOpts.src || moduleOpts + // Resolve module + let module + try { + if (typeof src === 'string') { + // Using ~ shorthand modules are resolved from project srcDir + if (src.indexOf('~') === 0) { + src = path.resolve(this.options.srcDir, src.substr(1)) + } + // eslint-disable-next-line no-eval + module = eval('require')(src) + } + } catch (e) { + // eslint-disable-next-line no-console + console.error('[Nuxt] Unable to resolve module', src) + // eslint-disable-next-line no-console + console.error(e) + return + } + // Validate module + if (!(module instanceof Function)) { + // eslint-disable-next-line no-console + console.error('[Nuxt] Module should be a function', module) + } + // Call module with `this` context and pass options + return new Promise((resolve, reject) => { + const result = module.call(this, options, err => { + if (err) { + return reject(err) + } + resolve(module) + }) + if (result && result.then instanceof Function) { + return result.then(resolve) + } + }) + } +} + +export default Module diff --git a/lib/nuxt.js b/lib/nuxt.js index 9e4f1f4eee..2cfc044772 100644 --- a/lib/nuxt.js +++ b/lib/nuxt.js @@ -6,6 +6,7 @@ import compression from 'compression' import fs from 'fs-extra' import pify from 'pify' import Server from './server' +import Module from './module' import * as build from './build' import * as render from './render' import generate from './generate' @@ -18,9 +19,16 @@ class Nuxt { var defaults = { dev: true, env: {}, - head: {}, + head: { + meta: [], + link: [], + style: [], + script: [] + }, plugins: [], css: [], + modules: [], + serverMiddlewares: [], cache: false, loading: { color: 'black', @@ -59,8 +67,11 @@ class Nuxt { this.options = _.defaultsDeep(options, defaults) // Env variables this.dev = this.options.dev + // Explicit srcDir and rootDir this.dir = (typeof options.rootDir === 'string' && options.rootDir ? options.rootDir : process.cwd()) this.srcDir = (typeof options.srcDir === 'string' && options.srcDir ? resolve(this.dir, options.srcDir) : this.dir) + options.rootDir = this.dir + options.srcDir = this.srcDir // If store defined, update store options to true if (fs.existsSync(join(this.srcDir, 'store'))) { this.options.store = true @@ -99,7 +110,16 @@ class Nuxt { this.generate = generate.bind(this) // Add this.utils (tests purpose) this.utils = utils - return this + // Add module integration + this.module = new Module(this) + // Install all modules in sequence and then return `this` instance + return utils.sequence(options.modules, this.module.installModule.bind(this.module)) + .then(() => this) + .catch((err) => { + console.error('[nuxt] error while initializing modules') // eslint-disable-line no-console + console.error(err) // eslint-disable-line no-console + process.exit(1) + }) } close (callback) { @@ -122,6 +142,10 @@ class Nuxt { if (this.pagesFilesWatcher) { this.pagesFilesWatcher.close() } + /* istanbul ignore if */ + if (this.customFilesWatcher) { + this.customFilesWatcher.close() + } return co(function * () { yield promises }) diff --git a/lib/server.js b/lib/server.js index 86a03760a9..cf9853e1a1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,15 +1,28 @@ 'use strict' const http = require('http') +const connect = require('connect') class Server { constructor (nuxt) { this.nuxt = nuxt - this.server = http.createServer(this.render.bind(this)) + // Initialize + this.app = connect() + this.server = http.createServer(this.app) + // Add Middlewares + this.nuxt.options.serverMiddlewares.forEach(m => { + if (m instanceof Function) { + this.app.use(m) + } else if (m && m.path && m.handler) { + this.app.use(m.path, m.handler) + } + }) + // Add default render middleware + this.app.use(this.render.bind(this)) return this } - render (req, res) { + render (req, res, next) { this.nuxt.render(req, res) return this } diff --git a/lib/utils.js b/lib/utils.js index 2ffc5aee7a..4ef641369c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -57,3 +57,19 @@ export function promisifyRoute (fn) { } return promise } + +export function sequence (tasks, fn) { + return tasks.reduce((promise, task) => promise.then(() => fn(task)), Promise.resolve()) +} + +export function chainFn (base, fn) { + if (!(fn instanceof Function)) { + return + } + return function () { + if (base instanceof Function) { + base.apply(this, arguments) + } + fn.apply(this, arguments) + } +} diff --git a/package.json b/package.json index 1320758da4..af9a7927a9 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "chokidar": "^1.6.1", "co": "^4.6.0", "compression": "^1.6.2", + "connect": "^3.6.1", "css-loader": "^0.28.1", "debug": "^2.6.6", "extract-text-webpack-plugin": "^2.1.0", diff --git a/test/basic.dev.test.js b/test/basic.dev.test.js index b8f8ed2b3a..6a8ebf38f7 100644 --- a/test/basic.dev.test.js +++ b/test/basic.dev.test.js @@ -13,7 +13,7 @@ test.before('Init Nuxt.js', async t => { rootDir: resolve(__dirname, 'fixtures/basic'), dev: true } - nuxt = new Nuxt(options) + nuxt = await new Nuxt(options) await nuxt.build() server = new nuxt.Server(nuxt) server.listen(port, 'localhost') diff --git a/test/basic.fail.generate.test.js b/test/basic.fail.generate.test.js index bdb3552f98..b96a8b343d 100644 --- a/test/basic.fail.generate.test.js +++ b/test/basic.fail.generate.test.js @@ -1,7 +1,7 @@ import test from 'ava' import { resolve } from 'path' -test('Fail with routes() which throw an error', t => { +test('Fail with routes() which throw an error', async t => { const Nuxt = require('../') const options = { rootDir: resolve(__dirname, 'fixtures/basic'), @@ -14,7 +14,7 @@ test('Fail with routes() which throw an error', t => { } } } - const nuxt = new Nuxt(options) + const nuxt = await new Nuxt(options) return new Promise((resolve) => { var oldExit = process.exit var oldCE = console.error // eslint-disable-line no-console diff --git a/test/basic.generate.test.js b/test/basic.generate.test.js index 3c423f3b84..737de6f6d8 100644 --- a/test/basic.generate.test.js +++ b/test/basic.generate.test.js @@ -17,7 +17,7 @@ test.before('Init Nuxt.js', async t => { let config = require(resolve(rootDir, 'nuxt.config.js')) config.rootDir = rootDir config.dev = false - nuxt = new Nuxt(config) + nuxt = await new Nuxt(config) try { await nuxt.generate() // throw an error (of /validate route) } catch (err) {} diff --git a/test/basic.test.js b/test/basic.test.js index 1c6c372456..27fe5a5922 100755 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -14,7 +14,7 @@ test.before('Init Nuxt.js', async t => { rootDir: resolve(__dirname, 'fixtures/basic'), dev: false } - nuxt = new Nuxt(options) + nuxt = await new Nuxt(options) await nuxt.build() server = new nuxt.Server(nuxt) server.listen(port, 'localhost') @@ -45,6 +45,7 @@ test('/stateful', async t => { test('/store', async t => { const { html } = await nuxt.renderRoute('/store') t.true(html.includes('

Vuex Nested Modules

')) + t.true(html.includes('

1

')) }) test('/head', async t => { diff --git a/test/children.test.js b/test/children.test.js index 776148cdc1..fd6a73396b 100644 --- a/test/children.test.js +++ b/test/children.test.js @@ -13,7 +13,7 @@ test.before('Init Nuxt.js', async t => { rootDir: resolve(__dirname, 'fixtures/children'), dev: false } - nuxt = new Nuxt(options) + nuxt = await new Nuxt(options) await nuxt.build() server = new nuxt.Server(nuxt) server.listen(port, 'localhost') diff --git a/test/dynamic-routes.test.js b/test/dynamic-routes.test.js index e52c09d718..3613144821 100644 --- a/test/dynamic-routes.test.js +++ b/test/dynamic-routes.test.js @@ -7,7 +7,7 @@ const readFile = pify(fs.readFile) // Init nuxt.js and create server listening on localhost:4000 test.before('Init Nuxt.js', async t => { const Nuxt = require('../') - const nuxt = new Nuxt({ + const nuxt = await new Nuxt({ rootDir: resolve(__dirname, 'fixtures/dynamic-routes'), dev: false }) diff --git a/test/error.test.js b/test/error.test.js index cca3f10147..400f0f8de2 100644 --- a/test/error.test.js +++ b/test/error.test.js @@ -13,7 +13,7 @@ test.before('Init Nuxt.js', async t => { rootDir: resolve(__dirname, 'fixtures/error'), dev: false } - nuxt = new Nuxt(options) + nuxt = await new Nuxt(options) await nuxt.build() server = new nuxt.Server(nuxt) server.listen(port, 'localhost') diff --git a/test/fixtures/basic/pages/store.vue b/test/fixtures/basic/pages/store.vue index d95f1d3868..32fbf8750a 100644 --- a/test/fixtures/basic/pages/store.vue +++ b/test/fixtures/basic/pages/store.vue @@ -1,5 +1,9 @@