diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..f124173 --- /dev/null +++ b/.babelrc @@ -0,0 +1,16 @@ +{ + "presets": [ + ["es2015", { "modules": false }], + "stage-2", + ], + "plugins": [ + "transform-runtime", + "transform-vue-jsx", + ], + "comments": false, + "env": { + "test": { + "plugins": ["istanbul"], + }, + }, +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e291365 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/template/.eslintignore b/.eslintignore similarity index 100% rename from template/.eslintignore rename to .eslintignore diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..d3f39ba --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,21 @@ +// http://eslint.org/docs/user-guide/configuring +module.exports = { + env: { + browser: true, + }, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module', + }, + plugins: [ + 'html', // required to lint *.vue files + ], + root: true, + rules: { + 'comma-dangle': ['error', 'always'], + 'indent': ['error', 4], + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, // allow debugger in development + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + }, +}; diff --git a/template/.gitignore b/.gitignore similarity index 52% rename from template/.gitignore rename to .gitignore index 8d95da1..6607c6d 100644 --- a/template/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store -node_modules/ -dist/ +/assets +/node_modules +/pages npm-debug.log -selenium-debug.log test/unit/coverage diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f90552e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2016-2017 Scott Bedard + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 2059ae7..5d9e93c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -
- # oc-vuetober-theme This project is an opinionated approach to single page applications within the wonderful worlds of [Vue.js](https://vuejs.org), [October CMS](https://octobercms.com), and [Laravel](https://laravel.com). To see what's included out of the box, check out the [live demo](http://vuetober.scottbedard.net). @@ -9,60 +5,52 @@ This project is an opinionated approach to single page applications within the w ### Getting started -Before getting started, make sure you have both [NPM](https://www.npmjs.com) and [vue-cli](https://github.com/vuejs/vue-cli) installed. Once that is taken care of, run the following command from your `/themes` directory. +In order to use this theme, you'll need to have a fair understanding of [Vue.js](https://vuejs.org). If you've never used this framework before, you can get the fundamentals down through [this free screencast series](http://vuecasts.com/). -```bash -$ vue init scottbedard/oc-vuetober-theme myawesometheme -$ cd myawesometheme -$ npm install -``` +To install a Vuetober theme, run the following commands from your `/themes` directory. -To build your site for production, run the following command. +> **Note:** As of version 2.1, pulling in `vue-cli` is no longer neccessary. ```bash -$ npm run build -``` +# clone down the repository and cd into it +$ git clone https://github.com/scottbedard/oc-vuetober-theme.git mytheme +$ cd mytheme -The development server can be fired up at `localhost:3000` by running the following. +# install dependencies +$ npm install -```bash -$ npm run dev +# run setup command +$ npm run setup ``` -For a better development experience, make sure to install the [Vue devtools](https://github.com/vuejs/vue-devtools) Chrome extension. +Other available commands - -### Routing +```bash +# compile production assets +$ npm run build -Routes can be registered from the `app/routes.js` file. Simply import your component, and assign it to a route. +# start development server +$ npm run dev -The layout components exist to provide an easy way of creating multiple site structures. To use a given layout for your page, make sure to register its route as a child route of the layout component. +# run unit tests +$ npm test +``` -When displaying dynamic content that may contain local links, it is recommended that you attach the `v-linkable` directive to the container. This will hijack click events and keep your user within the SPA. Without doing this, a full page reload will occur. + +### Features -```html - -``` +**Single file components** - -### Unit testing and continuous integration +Vue is a fantastic framework, and when using `.vue` files we have a great foundation for creating small, composable components. With this setup, we're able to write our code using any pre-processors we like, and take advantage of things like [hot reloading](https://vue-loader.vuejs.org/en/features/hot-reload.html) and [scoped css](https://vue-loader.vuejs.org/en/features/scoped-css.html). -This theme comes ready to go with unit testing and code coverage reporting. To run the test suite, execute the following command. +**State of the art tooling** -```bash -$ npm test -``` +This theme takes full advantage of [Webpack 2](https://webpack.js.org). The build script will optimize your application with techniques like automatically inlining small images, tree shaking, and more. In addition to this, [ESLint](http://eslint.org/docs/rules) will automatically fix your javascript to ensure a consistent style throughout your codebase. -Most continuous integration services like [Travic CI](https://travis-ci.org) and [Circle CI](https://circleci.com) should be able to infer the test command. Also, this theme works seamlessly with coverage reporting such as [Coveralls](https://coveralls.io). +**Modern Javascript** - -### Resources +You're free to use the latest and greatest Javascript and [JSX](https://github.com/vuejs/babel-plugin-transform-vue-jsx). Your code will be compiled by Babel into something that every browser can understand. -This theme uses quite a few different tools. If you've never used some of these before, it is highly recommended that you familiarize yourself with them by giving their docs the once over. +**Unit testing** -- [Karma](https://karma-runner.github.io/1.0/index.html) -- [Vue](http://vuejs.org/guide) -- [Vue Loader](http://vue-loader.vuejs.org) -- [Vue Resource](https://github.com/vuejs/vue-resource) -- [Vue Router](http://router.vuejs.org) -- [Webpack](https://webpack.github.io) +Every aspect of this theme is unit testable, with great tooling set up to help you deliver rock solid code. This includes the ability to easily render components in tests, mock dependencies, and even spy on functions. In addition to this, code coverage reporting is already configured, which means it's trivially easy to integrate with a service like [CodeCov](https://codecov.io). diff --git a/template/build/build.js b/build/build.js similarity index 64% rename from template/build/build.js rename to build/build.js index c38adab..c2a0067 100644 --- a/template/build/build.js +++ b/build/build.js @@ -1,27 +1,25 @@ // https://github.com/shelljs/shelljs -require('shelljs/global'); +require('./check-versions')(); -env.NODE_ENV = 'production'; +process.env.NODE_ENV = 'production'; -var config = require('../config'); var ora = require('ora'); var path = require('path'); +var chalk = require('chalk'); +var shell = require('shelljs'); var webpack = require('webpack'); +var config = require('../config'); var webpackConfig = require('./webpack.prod.conf'); -console.log( - ' Tip:\n' + - ' Built files are meant to be served over an HTTP server.\n' + - ' Opening index.htm over file:// won\'t work.\n' -); - var spinner = ora('building for production...'); spinner.start(); var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory); -rm('-rf', assetsPath); -mkdir('-p', assetsPath); -cp('-R', 'static/', assetsPath); +shell.rm('-rf', assetsPath); +shell.mkdir('-p', assetsPath); +shell.config.silent = true; +shell.cp('-R', 'static/*', assetsPath); +shell.config.silent = false; webpack(webpackConfig, function (err, stats) { spinner.stop(); @@ -36,5 +34,7 @@ webpack(webpackConfig, function (err, stats) { chunks: false, colors: true, modules: false, - }) + '\n'); + }) + '\n\n'); + + console.log(chalk.cyan(' Build complete.\n')); }); diff --git a/build/check-versions.js b/build/check-versions.js new file mode 100644 index 0000000..36ef7d0 --- /dev/null +++ b/build/check-versions.js @@ -0,0 +1,48 @@ +var chalk = require('chalk'); +var semver = require('semver'); +var packageConfig = require('../package.json'); + +function exec (cmd) { + return require('child_process').execSync(cmd).toString().trim(); +}; + +var versionRequirements = [ + { + name: 'node', + currentVersion: semver.clean(process.version), + versionRequirement: packageConfig.engines.node, + }, + { + name: 'npm', + currentVersion: exec('npm --version'), + versionRequirement: packageConfig.engines.npm, + }, +]; + +module.exports = function () { + var warnings = []; + for (var i = 0; i < versionRequirements.length; i++) { + var mod = versionRequirements[i]; + if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { + warnings.push( + mod.name + ': ' + + chalk.red(mod.currentVersion) + ' should be ' + + chalk.green(mod.versionRequirement) + ); + } + } + + if (warnings.length) { + console.log(''); + console.log(chalk.yellow('To use this template, you must update following to modules:')); + console.log(); + + for (var i = 0; i < warnings.length; i++) { + var warning = warnings[i]; + console.log(' ' + warning); + } + + console.log(); + process.exit(1); + } +}; diff --git a/template/build/dev-client.js b/build/dev-client.js similarity index 100% rename from template/build/dev-client.js rename to build/dev-client.js diff --git a/template/build/dev-server.js b/build/dev-server.js similarity index 63% rename from template/build/dev-server.js rename to build/dev-server.js index 4e8cd51..89636a7 100644 --- a/template/build/dev-server.js +++ b/build/dev-server.js @@ -1,6 +1,13 @@ -var browserSync = require('browser-sync'); +require('./check-versions')(); var config = require('../config'); + +if (!process.env.NODE_ENV) { + process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV); +} + +var browserSync = require('browser-sync'); var express = require('express'); +var opn = require('opn'); var path = require('path'); var proxyMiddleware = require('http-proxy-middleware'); var webpack = require('webpack'); @@ -9,7 +16,10 @@ var webpackConfig = process.env.NODE_ENV === 'testing' : require('./webpack.dev.conf'); // default port where dev server listens for incoming traffic -var port = process.env.PORT || config.dev.port; +var port = process.env.PORT; + +// automatically open browser, if not set will be false +var autoOpenBrowser = !!config.dev.autoOpenBrowser; // Define HTTP proxies to your custom API backend // https://github.com/chimurai/http-proxy-middleware @@ -20,13 +30,12 @@ var compiler = webpack(webpackConfig); var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: webpackConfig.output.publicPath, - stats: { - chunks: false, - colors: true, - }, + quiet: true, }); -var hotMiddleware = require('webpack-hot-middleware')(compiler); +var hotMiddleware = require('webpack-hot-middleware')(compiler, { + log: () => {}, +}); // force page reload when html-webpack-plugin template changes compiler.plugin('compilation', function (compilation) { @@ -41,22 +50,29 @@ Object.keys(proxyTable).forEach(function (context) { var options = proxyTable[context]; if (typeof options === 'string') { - options = { target: options } + options = { target: options }; } - app.use(proxyMiddleware(context, options)); + app.use(proxyMiddleware(options.filter || context, options)); }); // handle fallback for HTML5 history API app.use(require('connect-history-api-fallback')()); +// serve webpack bundle output +app.use(devMiddleware); + +// enable hot-reload and state-preserving +// compilation error display +app.use(hotMiddleware); + // serve pure static assets var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory); app.use(staticPath, express.static('./static')); // browser sync browserSync({ - proxy: "{{ proxy }}", + proxy: 'http://vuetober.october.dev', open: false, middleware: [ devMiddleware, @@ -64,7 +80,7 @@ browserSync({ ], rewriteRules: [ { - match: //ig, + match: //ig, fn: function(match) { return ''; }, @@ -72,17 +88,26 @@ browserSync({ { match: /Note: These videos have not been created yet.
+ Getting started & creating pages + Creating global components + Communicating with the backend + Customizing the build setup + The basics of unit testing + Continuous integration and code coverage + Bundle splitting + State management with Vuex +Page not found.
-To see the blog example in action, make sure you have the following plugins installed.
- -'; + return '
'; }, }, ], }); +var uri = 'http://localhost:3000'; + +devMiddleware.waitUntilValid(function () { + console.log('> Listening at ' + uri + '\n'); +}); + module.exports = app.listen(port, function (err) { if (err) { console.log(err); return; } - console.log('Listening at http://localhost:3000' + '\n'); + // when env is testing, don't need open it + if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { + opn(uri); + } }); diff --git a/build/setup.js b/build/setup.js new file mode 100644 index 0000000..07cea77 --- /dev/null +++ b/build/setup.js @@ -0,0 +1,124 @@ +'use strict'; + +const chalk = require('chalk'); +const fs = require('fs'); +const path = require('path'); +const prompt = require('prompt'); +const rimraf = require('rimraf'); + +const themeDirectory = path.basename(path.resolve(__dirname, '..')); + +const schema = { + properties: { + name: { + default: themeDirectory, + description: chalk.gray(' Name'), + required: true, + }, + description: { + description: chalk.gray(' Description'), + required: false, + }, + author: { + description: chalk.gray(' Author'), + required: false, + }, + url: { + description: chalk.gray(' Development url'), + required: true, + }, + cleanup: { + before: value => Boolean(value.match(/^[Yy](.*)/)), + default: 'y', + description: chalk.gray(' Remove setup command (y/n)'), + message: 'Answer must be yes or no', + pattern: /^[YyNn](.*)*/, + required: true, + }, + }, +}; + +// +// if they haven't removed our repo yet, offer to do it for them +// +let isVuetoberRepository; +try { + const gitConfig = fs.readFileSync(path.resolve(__dirname, '../.git/config'), 'utf8'); + isVuetoberRepository = Boolean(gitConfig.match(/github\.com[\/:]scottbedard\/oc-vuetober-theme/g)); +} catch (err) { + isVuetoberRepository = false; +} + +if (isVuetoberRepository) { + schema.properties.cleanupGit = { + before: value => Boolean(value.match(/^[Yy](.*)/)), + default: 'y', + description: chalk.gray(' Remove git repository (y/n)'), + pattern: /^[YyNn](.*)*/, + required: true, + }; +} + +// +// start the setup prompt +// +console.log (chalk.white(' Setup a new Vuetober theme')); +console.log (); + +prompt.message = false; +prompt.start(); + +// +// process the answers the user provided +// +prompt.get(schema, function (err, result) { + if (err) { + console.log (); + console.log (); + console.log (chalk.red(' Setup canceled')); + return; + } + + // create our theme.yaml file + const theme = + `name: '${ result.name }'\n` + + `description: '${ result.description }'\n` + + `author: '${ result.author }'`; + + fs.writeFileSync(path.resolve(__dirname, '../theme.yaml'), theme); + + // update our dev-server to use the correct url and directory + const devServer = fs.readFileSync(path.resolve(__dirname, './dev-server.js'), 'utf8') + .replace(/oc-vuetober-theme/g, themeDirectory) + .replace(/http:\/\/vuetober\.october\.dev/g, result.url); + + fs.writeFileSync(path.resolve(__dirname, './dev-server.js'), devServer); + + // update the asset path to use the correct directory + const configIndex = fs.readFileSync(path.resolve(__dirname, '../config/index.js'), 'utf8') + .replace(/oc-vuetober-theme/g, themeDirectory); + + fs.writeFileSync(path.resolve(__dirname, '../config/index.js'), configIndex); + + // remove the setup command + if (result.cleanup) { + var setupPath = path.resolve(__dirname, 'setup.js'); + var packageJson = fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8') + .replace('"setup": "node build/setup.js",\n ', ''); + + fs.writeFileSync(path.resolve(__dirname, '../package.json'), packageJson); + rimraf.sync(setupPath); + } + + // remove the old git repository + if (result.cleanupGit) { + rimraf.sync(path.resolve(__dirname, '../.git')); + } + + console.log (); + console.log (chalk.green(' Vuetober set up complete, time to build something amazing!')); + console.log (); + console.log (chalk.gray(' You\'ll need to run the following command before viewing your theme in the browser.')); + console.log (); + console.log (chalk.gray(' $ npm run build')); +}); diff --git a/template/build/utils.js b/build/utils.js similarity index 81% rename from template/build/utils.js rename to build/utils.js index 186fc68..2f0b186 100644 --- a/template/build/utils.js +++ b/build/utils.js @@ -17,6 +17,7 @@ exports.cssLoaders = function (options) { function generateLoaders (loaders) { var sourceLoader = loaders.map(function (loader) { var extraParamChar; + if (/\?/.test(loader)) { loader = loader.replace(/\?/, '-loader?'); extraParamChar = '&'; @@ -28,14 +29,19 @@ exports.cssLoaders = function (options) { return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : ''); }).join('!'); + // Extract CSS when that option is specified + // (which is the case during production build) if (options.extract) { - return ExtractTextPlugin.extract('vue-style-loader', sourceLoader); + return ExtractTextPlugin.extract({ + use: sourceLoader, + fallback: 'vue-style-loader', + }); } else { return ['vue-style-loader', sourceLoader].join('!'); } - }; + } - // http://vuejs.github.io/vue-loader/configurations/extract-css.html + // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html return { css: generateLoaders(['css']), less: generateLoaders(['css', 'less']), @@ -53,10 +59,10 @@ exports.styleLoaders = function (options) { var loaders = exports.cssLoaders(options); for (var extension in loaders) { - var loader = loaders[extension]; + var loader = loaders[extension] output.push({ test: new RegExp('\\.' + extension + '$'), - loader: loader + loader: loader, }); } diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js new file mode 100644 index 0000000..a140cae --- /dev/null +++ b/build/webpack.base.conf.js @@ -0,0 +1,103 @@ +var config = require('../config'); +var eslintFriendlyFormatter = require('eslint-friendly-formatter'); +var isProduction = process.env.NODE_ENV === 'production'; +var path = require('path'); +var utils = require('./utils'); +var webpack = require('webpack'); + +function resolve (dir) { + return path.join(__dirname, '..', dir); +}; + +module.exports = { + entry: { + app: './src/main.js', + }, + output: { + path: config.build.assetsRoot, + filename: '[name].js', + publicPath: isProduction + ? config.build.assetsPublicPath + : config.dev.assetsPublicPath, + }, + resolve: { + extensions: ['.js', '.vue', '.json', '.scss'], + modules: [ + resolve('src'), + resolve('scss'), + resolve('node_modules'), + ], + alias: { + 'src': resolve('src'), + 'assets': resolve('src/assets'), + 'components': resolve('src/components'), + }, + }, + module: { + rules: [ + { + test: /\.(js|vue)$/, + loader: 'eslint-loader', + enforce: "pre", + include: [resolve('src'), resolve('test')], + options: { + fix: true, + formatter: eslintFriendlyFormatter, + }, + }, + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + loaders: utils.cssLoaders({ + extract: isProduction, + sourceMap: isProduction + ? config.build.productionSourceMap + : config.dev.cssSourceMap, + }), + postcss: [ + require('autoprefixer')({ browsers: ['last 2 versions'] }), + ], + }, + }, + { + test: /\.js$/, + loader: 'babel-loader', + include: [resolve('src'), resolve('test')], + }, + { + test: /\.json$/, + loader: 'json-loader', + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader', + query: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]'), + }, + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + query: { + limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]'), + }, + }, + ], + }, + plugins: [ + new webpack.LoaderOptionsPlugin({ + options: { + context: '/', + sassLoader: { + includePaths: [ + resolve('scss'), + resolve('node_modules'), + ], + }, + }, + }), + ], +}; diff --git a/template/build/webpack.dev.conf.js b/build/webpack.dev.conf.js similarity index 71% rename from template/build/webpack.dev.conf.js rename to build/webpack.dev.conf.js index 6aa75b9..e5a3ec4 100644 --- a/template/build/webpack.dev.conf.js +++ b/build/webpack.dev.conf.js @@ -1,5 +1,6 @@ var baseWebpackConfig = require('./webpack.base.conf'); var config = require('../config'); +var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var merge = require('webpack-merge'); var utils = require('./utils'); @@ -12,20 +13,20 @@ Object.keys(baseWebpackConfig.entry).forEach(function (name) { module.exports = merge(baseWebpackConfig, { module: { - loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }), + rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }), }, - devtool: '#eval-source-map', // eval-source-map is faster for development + devtool: '#cheap-module-eval-source-map', // cheap-module-eval-source-map is faster for development plugins: [ new webpack.DefinePlugin({ 'process.env': config.dev.env }), // https://github.com/glenjamin/webpack-hot-middleware#installation--usage - new webpack.optimize.OccurenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin(), + new webpack.NoEmitOnErrorsPlugin(), // https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ - filename: 'src/index.htm', + filename: 'src/index.html', inject: true, template: 'src/index.htm', }), + new FriendlyErrorsPlugin(), ], }); diff --git a/template/build/webpack.prod.conf.js b/build/webpack.prod.conf.js similarity index 79% rename from template/build/webpack.prod.conf.js rename to build/webpack.prod.conf.js index e568211..2785065 100644 --- a/template/build/webpack.prod.conf.js +++ b/build/webpack.prod.conf.js @@ -12,58 +12,51 @@ var env = process.env.NODE_ENV === 'testing' var webpackConfig = merge(baseWebpackConfig, { module: { - loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }), + rules: utils.styleLoaders({ + extract: true, + sourceMap: config.build.productionSourceMap, + }), }, devtool: config.build.productionSourceMap ? '#source-map' : false, output: { - path: config.build.assetsRoot, - filename: utils.assetsPath('js/[name].[chunkhash].js'), chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'), - }, - vue: { - loaders: utils.cssLoaders({ - sourceMap: config.build.productionSourceMap, - extract: true, - }), + filename: utils.assetsPath('js/[name].[chunkhash].js'), + path: config.build.assetsRoot, }, plugins: [ - // http://vuejs.github.io/vue-loader/workflow/production.html + // http://vuejs.github.io/vue-loader/en/workflow/production.html new webpack.DefinePlugin({ 'process.env': env }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }}), - new webpack.optimize.OccurenceOrderPlugin(), - // extract css into its own file new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), - - // generate dist index.htm with correct asset hash for caching. - // you can customize output by editing /index.htm - // see https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ - filename: process.env.NODE_ENV === 'testing' ? 'index.htm' : config.build.index, - template: 'src/index.htm', + chunksSortMode: 'dependency', + filename: path.resolve(__dirname, '../pages/index.htm'), inject: true, minify: { + // https://github.com/kangax/html-minifier#options-quick-reference collapseWhitespace: true, removeAttributeQuotes: true, removeComments: true, - // https://github.com/kangax/html-minifier#options-quick-reference }, - chunksSortMode: 'dependency', + template: 'src/index.htm', }), - // split vendor js into its own file new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function (module, count) { + // any required modules inside node_modules are extracted to vendor return module.resource && /\.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, '../node_modules')) === 0; }, }), - // extract webpack runtime and module manifest to its own file in order to // prevent vendor hash from being updated whenever app bundle is updated - new webpack.optimize.CommonsChunkPlugin({ chunks: ['vendor'], name: 'manifest' }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest', + chunks: ['vendor'], + }), ], }); @@ -81,4 +74,9 @@ if (config.build.productionGzip) { ); } +if (config.build.bundleAnalyzerReport) { + var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; + webpackConfig.plugins.push(new BundleAnalyzerPlugin()); +} + module.exports = webpackConfig; diff --git a/build/webpack.test.conf.js b/build/webpack.test.conf.js new file mode 100644 index 0000000..f3a243f --- /dev/null +++ b/build/webpack.test.conf.js @@ -0,0 +1,28 @@ +var baseConfig = require('./webpack.base.conf'); +var merge = require('webpack-merge'); +var utils = require('./utils'); +var webpack = require('webpack'); + +var webpackConfig = merge(baseConfig, { + // use inline sourcemap for karma-sourcemap-loader + devtool: '#inline-source-map', + module: { + rules: utils.styleLoaders(), + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': require('../config/test.env'), + }), + ], + resolve: { + alias: { + // include the template compiler in our test environment + 'vue$': 'vue/dist/vue.common.js', + }, + }, +}); + +// no need for app entry during tests +delete webpackConfig.entry; + +module.exports = webpackConfig; diff --git a/template/config/dev.env.js b/config/dev.env.js similarity index 100% rename from template/config/dev.env.js rename to config/dev.env.js diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..65d80f9 --- /dev/null +++ b/config/index.js @@ -0,0 +1,23 @@ +// see http://vuejs-templates.github.io/webpack for documentation. +var path = require('path'); + +module.exports = { + build: { + assetsPublicPath: '/themes/oc-vuetober-theme/assets/', + assetsRoot: path.resolve(__dirname, '../assets'), + assetsSubDirectory: 'static', + bundleAnalyzerReport: false, + env: require('./prod.env'), + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + productionSourceMap: true, + }, + dev: { + assetsPublicPath: '/', + assetsSubDirectory: 'static', + autoOpenBrowser: true, + cssSourceMap: false, + env: require('./dev.env'), + proxyTable: {}, + }, +}; diff --git a/template/config/prod.env.js b/config/prod.env.js similarity index 100% rename from template/config/prod.env.js rename to config/prod.env.js diff --git a/template/config/test.env.js b/config/test.env.js similarity index 100% rename from template/config/test.env.js rename to config/test.env.js diff --git a/meta.json b/meta.json deleted file mode 100644 index 285f35f..0000000 --- a/meta.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "prompts": { - "name": { - "type": "string", - "required": true, - "message": "Project name" - }, - "description": { - "type": "string", - "required": false, - "message": "Project description", - "default": "A Vue.js project" - }, - "author": { - "type": "string", - "message": "Author" - }, - "proxy": { - "type": "string", - "required": true, - "default": "http://vuetober.october.dev" - }, - "lint": { - "type": "confirm", - "message": "Use ESLint to lint your code?" - }, - "lintConfig": { - "when": "lint", - "type": "list", - "message": "Pick an ESLint preset", - "choices": [ - { - "name": "none (configure it yourself)", - "value": "none", - "short": "none" - }, - { - "name": "Standard (https://github.com/feross/standard)", - "value": "standard", - "short": "Standard" - }, - { - "name": "AirBNB (https://github.com/airbnb/javascript)", - "value": "airbnb", - "short": "AirBNB" - } - ] - }, - "unit": { - "type": "confirm", - "message": "Setup unit tests with Karma + Mocha?" - } - }, - "filters": { - ".eslintrc.js": "lint", - "test/unit/**/*": "unit" - }, - "completeMessage": "To get started:\n\n cd {{destDirName}}\n npm install\n npm run dev\n\nDocumentation can be found at https://vuejs-templates.github.io/webpack" -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..446bb71 --- /dev/null +++ b/package.json @@ -0,0 +1,90 @@ +{ + "name": "oc-vuetober-theme", + "version": "2.1.0", + "description": "An opinionated approach to single page applications within the wonderful worlds of Vue.js, October CMS, and Laravel.", + "author": "scott@scottbedard.net", + "private": true, + "scripts": { + "build": "node build/build.js", + "dev": "node build/dev-server.js", + "lint": "eslint --ext .js,.vue src test/unit/specs --fix", + "setup": "node build/setup.js", + "test": "npm run unit", + "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run" + }, + "dependencies": { + "axios": "^0.15.3", + "vue": "^2.1.10", + "vue-router": "^2.2.0" + }, + "devDependencies": { + "autoprefixer": "^6.7.2", + "babel-core": "^6.22.1", + "babel-eslint": "^7.1.1", + "babel-helper-vue-jsx-merge-props": "^2.0.2", + "babel-loader": "^6.2.10", + "babel-plugin-istanbul": "^3.1.2", + "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-runtime": "^6.22.0", + "babel-plugin-transform-vue-jsx": "^3.3.0", + "babel-preset-es2015": "^6.22.0", + "babel-preset-stage-2": "^6.22.0", + "babel-register": "^6.22.0", + "browser-sync": "^2.18.7", + "chai": "^3.5.0", + "chalk": "^1.1.3", + "compression-webpack-plugin": "^0.3.2", + "connect-history-api-fallback": "^1.3.0", + "cross-env": "^3.1.4", + "css-loader": "^0.26.1", + "eslint": "^3.14.1", + "eslint-friendly-formatter": "^2.0.7", + "eslint-loader": "^1.6.1", + "eslint-plugin-html": "^2.0.0", + "eventsource-polyfill": "^0.9.6", + "express": "^4.14.1", + "extract-text-webpack-plugin": "^2.0.0-rc.2", + "file-loader": "^0.10.0", + "friendly-errors-webpack-plugin": "^1.1.3", + "function-bind": "^1.1.0", + "html-webpack-plugin": "^2.28.0", + "http-proxy-middleware": "^0.17.3", + "inject-loader": "^2.0.1", + "json-loader": "^0.5.4", + "karma": "^1.4.1", + "karma-coverage": "^1.1.1", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-sinon-chai": "^1.2.4", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.26", + "karma-webpack": "^2.0.2", + "lolex": "^1.5.2", + "mocha": "^3.2.0", + "node-sass": "^4.5.0", + "normalize.scss": "^0.1.0", + "opn": "^4.0.2", + "ora": "^1.1.0", + "phantomjs-prebuilt": "^2.1.14", + "prompt": "^1.0.0", + "rimraf": "^2.5.4", + "sass-loader": "^4.1.1", + "semver": "^5.3.0", + "shelljs": "^0.7.6", + "sinon": "^1.17.7", + "sinon-chai": "^2.8.0", + "url-loader": "^0.5.7", + "vue-loader": "^10.3.0", + "vue-style-loader": "^2.0.0", + "vue-template-compiler": "^2.1.10", + "webpack": "^2.2.1", + "webpack-bundle-analyzer": "^2.2.1", + "webpack-dev-middleware": "^1.10.0", + "webpack-hot-middleware": "^2.16.1", + "webpack-merge": "^2.6.1" + }, + "engines": { + "node": ">= 4.0.0", + "npm": ">= 3.0.0" + } +} diff --git a/template/src/scss/classes/transitions.scss b/scss/classes/transitions.scss similarity index 57% rename from template/src/scss/classes/transitions.scss rename to scss/classes/transitions.scss index 0364fab..8b624ca 100644 --- a/template/src/scss/classes/transitions.scss +++ b/scss/classes/transitions.scss @@ -1,9 +1,9 @@ .fade-enter-active, .fade-leave-active { - transition: opacity .2s ease; + transition: opacity 200ms ease-in-out; } .fade-enter, .fade-leave-active { - opacity: 0 + opacity: 0; } diff --git a/scss/core.scss b/scss/core.scss new file mode 100644 index 0000000..2f5ae48 --- /dev/null +++ b/scss/core.scss @@ -0,0 +1,8 @@ +// This file is used as a single include point for all of +// your scss that does not output any css. These might +// include variables, mixins, and functions, etc... + +// +// Variables +// +@import './variables/colors'; diff --git a/scss/elements/anchors.scss b/scss/elements/anchors.scss new file mode 100644 index 0000000..a12ab23 --- /dev/null +++ b/scss/elements/anchors.scss @@ -0,0 +1,8 @@ +a { + color: $vue-green; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} diff --git a/scss/elements/headers.scss b/scss/elements/headers.scss new file mode 100644 index 0000000..8182808 --- /dev/null +++ b/scss/elements/headers.scss @@ -0,0 +1,4 @@ +h1, +h2 { + font-weight: 300; +} diff --git a/template/src/scss/elements/html_body.scss b/scss/elements/html_body.scss similarity index 81% rename from template/src/scss/elements/html_body.scss rename to scss/elements/html_body.scss index 7c981d4..ffc47ad 100644 --- a/template/src/scss/elements/html_body.scss +++ b/scss/elements/html_body.scss @@ -11,4 +11,6 @@ html { body { font-family: 'Open Sans', sans-serif; + min-height: 100%; + padding: 20px; } diff --git a/scss/global.scss b/scss/global.scss new file mode 100644 index 0000000..beb0d13 --- /dev/null +++ b/scss/global.scss @@ -0,0 +1,21 @@ +// This file should reference all of our global styles +// that output css. It should be included only from +// root component from a non-scoped style block. + +// +// Dependencies +// +@import 'core'; +@import 'normalize.scss/normalize.scss'; + +// +// Elements +// +@import './elements/anchors'; +@import './elements/headers'; +@import './elements/html_body'; + +// +// Classes +// +@import './classes/transitions'; diff --git a/template/src/scss/core.scss b/scss/variables/colors.scss similarity index 52% rename from template/src/scss/core.scss rename to scss/variables/colors.scss index e6eec33..f356783 100644 --- a/template/src/scss/core.scss +++ b/scss/variables/colors.scss @@ -1,4 +1 @@ -// -// Variables -// $vue-green: #41b883; diff --git a/src/app/boot.js b/src/app/boot.js new file mode 100644 index 0000000..dcb2445 --- /dev/null +++ b/src/app/boot.js @@ -0,0 +1,24 @@ +import axios from 'axios'; +import Vue from 'vue'; + +// +// Configure our HTTP client +// +let token = document.querySelector('meta[name=token]').getAttribute('content'); +axios.defaults.headers.common['X-CSRF-TOKEN'] = token; + +// +// Plugins +// +require('./plugins/router'); + +// +// Directives +// +require('./directives/linkable'); + +// +// Register global components +// +const components = require('src/components/global'); +Object.keys(components).forEach(tag => Vue.component(tag, components[tag])); diff --git a/src/app/directives/linkable.js b/src/app/directives/linkable.js new file mode 100644 index 0000000..6e38bb0 --- /dev/null +++ b/src/app/directives/linkable.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; + +Vue.directive('linkable', { + bind(el, binding, vnode) { + const component = vnode.context; + + el.linkableClick = (e) => { + for (const clickedElement of e.path) { + if (clickedElement === el || clickedElement.tagName === 'A') { + if (clickedElement.hostname === window.location.hostname) { + e.preventDefault(); + component.$router.push(clickedElement.pathname); + } + break; + } + } + }; + + el.addEventListener('click', el.linkableClick); + }, + unbind(el) { + el.removeEventListener('click', el.linkableClick); + }, +}); diff --git a/src/app/plugins/router.js b/src/app/plugins/router.js new file mode 100644 index 0000000..a131d46 --- /dev/null +++ b/src/app/plugins/router.js @@ -0,0 +1,4 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; + +Vue.use(VueRouter); diff --git a/src/app/routes.js b/src/app/routes.js new file mode 100644 index 0000000..21a4861 --- /dev/null +++ b/src/app/routes.js @@ -0,0 +1,12 @@ +export default [ + { + name: 'home', + path: '/', + component: require('src/pages/home/home'), + }, + { + name: 'videos', + path: '/videos', + component: require('src/pages/videos/videos'), + }, +]; diff --git a/template/src/assets/october.png b/src/assets/october.png similarity index 100% rename from template/src/assets/october.png rename to src/assets/october.png diff --git a/template/src/assets/vue.png b/src/assets/vue.png similarity index 100% rename from template/src/assets/vue.png rename to src/assets/vue.png diff --git a/src/components/global.js b/src/components/global.js new file mode 100644 index 0000000..12cb08b --- /dev/null +++ b/src/components/global.js @@ -0,0 +1,5 @@ +// These components are registered globally, and can be used +// anywhere within our application without importing them. +export default { + +}; diff --git a/template/src/index.htm b/src/index.htm similarity index 61% rename from template/src/index.htm rename to src/index.htm index 53dbbfa..68c7af2 100644 --- a/template/src/index.htm +++ b/src/index.htm @@ -1,16 +1,26 @@ title = "Index" url = "/:all?*" -description = "Do not modify" is_hidden = 0 == +function onStart() +{ + $this['token'] = \Session::token(); +} +==
-
+ + - + + + + + +