From 1b6234f548f63753f0a046b5190f6ca14d16df28 Mon Sep 17 00:00:00 2001 From: dustin-H Date: Sat, 12 Aug 2017 23:32:34 +0200 Subject: [PATCH] initial commit --- .babelrc | 25 ++++++ .editorconfig | 9 +++ .eslintignore | 1 + .eslintrc.js | 19 +++++ .gitignore | 7 ++ .stylelintrc | 7 ++ CONTRIBUTING.md | 36 +++++++++ LICENSE | 20 +++++ README.md | 79 ++++++++++++++++++ build/build.js | 121 +++++++++++++++++++++++++++ build/utils/index.js | 66 +++++++++++++++ build/utils/log.js | 27 +++++++ build/utils/style.js | 66 +++++++++++++++ build/utils/write.js | 19 +++++ build/webpack.config.base.js | 59 ++++++++++++++ build/webpack.config.dev.js | 97 ++++++++++++++++++++++ build/webpack.config.dll.js | 28 +++++++ package.json | 139 ++++++++++++++++++++++++++++++++ src/StyleProvider.vue | 16 ++++ src/fela.js | 55 +++++++++++++ src/index.js | 13 +++ test/.eslintrc | 9 +++ test/helpers/Test.vue | 107 ++++++++++++++++++++++++ test/helpers/index.js | 51 ++++++++++++ test/helpers/utils.js | 89 ++++++++++++++++++++ test/helpers/wait-for-update.js | 56 +++++++++++++ test/index.js | 32 ++++++++ test/karma.conf.js | 44 ++++++++++ test/specs/Hello-jsx.spec.js | 20 +++++ test/specs/Hello.spec.js | 11 +++ test/visual.js | 69 ++++++++++++++++ 31 files changed, 1397 insertions(+) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .stylelintrc create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build/build.js create mode 100644 build/utils/index.js create mode 100644 build/utils/log.js create mode 100644 build/utils/style.js create mode 100644 build/utils/write.js create mode 100644 build/webpack.config.base.js create mode 100644 build/webpack.config.dev.js create mode 100644 build/webpack.config.dll.js create mode 100644 package.json create mode 100644 src/StyleProvider.vue create mode 100644 src/fela.js create mode 100644 src/index.js create mode 100644 test/.eslintrc create mode 100644 test/helpers/Test.vue create mode 100644 test/helpers/index.js create mode 100644 test/helpers/utils.js create mode 100644 test/helpers/wait-for-update.js create mode 100644 test/index.js create mode 100644 test/karma.conf.js create mode 100644 test/specs/Hello-jsx.spec.js create mode 100644 test/specs/Hello.spec.js create mode 100644 test/visual.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..cee0f3a --- /dev/null +++ b/.babelrc @@ -0,0 +1,25 @@ +{ + "presets": [ + [ + "env", + { + "targets": { + "browsers": [ + "last 2 versions" + ] + } + } + ] + ], + "plugins": [ + "transform-vue-jsx", + "transform-object-rest-spread" + ], + "env": { + "test": { + "plugins": [ + "istanbul" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..5f98501 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +dist/*.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..c62d0bd --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + extends: 'vue', + // add your custom rules here + 'rules': { + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + }, + globals: { + requestAnimationFrame: true, + performance: true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d22c773 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +node_modules/ +npm-debug.log +test/coverage +dist +yarn-error.log +reports diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000..31f81bc --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,7 @@ +{ + "processors": ["stylelint-processor-html"], + "extends": "stylelint-config-standard", + "rules": { + "no-empty-source": null + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f46265c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/dustin-h/vue-fela). + + +## Pull Requests + +- **Keep the same style** - eslint will automatically be ran before committing + +- **Tip** to pass lint tests easier use the `npm run lint:fix` command + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. + +- **Create feature branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure your commits message means something + + +## Running Tests + +Launch visual tests and watch the components at the same time + +``` bash +$ npm run dev +``` + + +**Happy coding**! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ffc5de8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 Dustin Hoffn + +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 new file mode 100644 index 0000000..8a23568 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# vue-fela + +[![npm](https://img.shields.io/npm/v/vue-fela.svg)](https://www.npmjs.com/package/vue-fela) [![vue2](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://vuejs.org/) + +> Fela bindings for vue + +## Installation + +```bash +npm install --save vue-fela +``` + +## Usage + +### Bundler (Webpack, Rollup) + +```js +import Vue from 'vue' +import vue-fela from 'vue-fela' +// You need a specific loader for CSS files like https://github.com/webpack/css-loader +import 'vue-fela/dist/vue-fela.css' + +Vue.use(vue-fela) +``` + +### Browser + +```html + + + + + + + + +``` + +## Development + +### Launch visual tests + +```bash +npm run dev +``` + +### Launch Karma with coverage + +```bash +npm run dev:coverage +``` + +### Build + +Bundle the js and css of to the `dist` folder: + +```bash +npm run build +``` + + +## Publishing + +The `prepublish` hook will ensure dist files are created before publishing. This +way you don't need to commit them in your repository. + +```bash +# Bump the version first +# It'll also commit it and create a tag +npm version +# Push the bumped package and tags +git push --follow-tags +# Ship it 🚀 +npm publish +``` + +## License + +[MIT](http://opensource.org/licenses/MIT) diff --git a/build/build.js b/build/build.js new file mode 100644 index 0000000..34fe96a --- /dev/null +++ b/build/build.js @@ -0,0 +1,121 @@ +const mkdirp = require('mkdirp') +const rollup = require('rollup').rollup +const vue = require('rollup-plugin-vue') +const jsx = require('rollup-plugin-jsx') +const buble = require('rollup-plugin-buble') +const replace = require('rollup-plugin-replace') +const cjs = require('rollup-plugin-commonjs') +const node = require('rollup-plugin-node-resolve') +const uglify = require('uglify-js') +const CleanCSS = require('clean-css') + +// Make sure dist dir exists +mkdirp('dist') + +const { + logError, + write, + banner, + name, + moduleName, + version, + processStyle +} = require('./utils') + +function rollupBundle ({ env }) { + return rollup({ + entry: 'src/index.js', + plugins: [ + node({ + extensions: ['.js', '.jsx', '.vue'] + }), + cjs(), + vue({ + compileTemplate: true, + css (styles, stylesNodes) { + // Only generate the styles once + if (env['process.env.NODE_ENV'] === '"production"') { + Promise.all( + stylesNodes.map(processStyle) + ).then(css => { + const result = css.map(c => c.css).join('') + // write the css for every component + // TODO add it back if we extract all components to individual js + // files too + // css.forEach(writeCss) + write(`dist/${name}.css`, result) + write(`dist/${name}.min.css`, new CleanCSS().minify(result).styles) + }).catch(logError) + } + } + }), + jsx({ factory: 'h' }), + replace(Object.assign({ + __VERSION__: version + }, env)), + buble({ + objectAssign: 'Object.assign' + }) + ] + }) +} + +const bundleOptions = { + banner, + exports: 'named', + format: 'umd', + moduleName +} + +function createBundle ({ name, env, format }) { + return rollupBundle({ + env + }).then(function (bundle) { + const options = Object.assign({}, bundleOptions) + if (format) options.format = format + const code = bundle.generate(options).code + if (/min$/.test(name)) { + const minified = uglify.minify(code, { + output: { + preamble: banner, + ascii_only: true // eslint-disable-line camelcase + } + }).code + return write(`dist/${name}.js`, minified) + } else { + return write(`dist/${name}.js`, code) + } + }).catch(logError) +} + +// Browser bundle (can be used with script) +createBundle({ + name: `${name}`, + env: { + 'process.env.NODE_ENV': '"development"' + } +}) + +// Commonjs bundle (preserves process.env.NODE_ENV) so +// the user can replace it in dev and prod mode +createBundle({ + name: `${name}.common`, + env: {}, + format: 'cjs' +}) + +// uses export and import syntax. Should be used with modern bundlers +// like rollup and webpack 2 +createBundle({ + name: `${name}.esm`, + env: {}, + format: 'es' +}) + +// Minified version for browser +createBundle({ + name: `${name}.min`, + env: { + 'process.env.NODE_ENV': '"production"' + } +}) diff --git a/build/utils/index.js b/build/utils/index.js new file mode 100644 index 0000000..ff22191 --- /dev/null +++ b/build/utils/index.js @@ -0,0 +1,66 @@ +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const { join } = require('path') + +const { + red, + logError +} = require('./log') + +const { + processStyle +} = require('./style') + +const uppercamelcase = require('uppercamelcase') + +exports.write = require('./write') + +const { + author, + name, + version, + dllPlugin +} = require('../../package.json') + +const authorName = author.replace(/\s+<.*/, '') +const minExt = process.env.NODE_ENV === 'production' ? '.min' : '' + +exports.author = authorName +exports.version = version +exports.dllName = dllPlugin.name +exports.moduleName = uppercamelcase(name) +exports.name = name +exports.filename = name + minExt +exports.banner = `/*! + * ${name} v${version} + * (c) ${new Date().getFullYear()} ${authorName} + * Released under the MIT License. + */ +` + +// log.js +exports.red = red +exports.logError = logError + +// It'd be better to add a sass property to the vue-loader options +// but it simply don't work +const sassOptions = { + includePaths: [ + join(__dirname, '../../node_modules') + ] +} + +// don't extract css in test mode +const nullLoader = process.env.NODE_ENV === 'common' ? 'null-loader!' : '' +exports.vueLoaders = + process.env.BABEL_ENV === 'test' ? { + css: 'css-loader', + scss: `css-loader!sass-loader?${JSON.stringify(sassOptions)}` + } : { + css: ExtractTextPlugin.extract(`${nullLoader}css-loader`), + scss: ExtractTextPlugin.extract( + `${nullLoader}css-loader!sass-loader?${JSON.stringify(sassOptions)}` + ) + } + +// style.js +exports.processStyle = processStyle diff --git a/build/utils/log.js b/build/utils/log.js new file mode 100644 index 0000000..2dd2050 --- /dev/null +++ b/build/utils/log.js @@ -0,0 +1,27 @@ +function logError (e) { + console.log(e) +} + +function blue (str) { + return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m` +} + +function green (str) { + return `\x1b[1m\x1b[32m${str}\x1b[39m\x1b[22m` +} + +function red (str) { + return `\x1b[1m\x1b[31m${str}\x1b[39m\x1b[22m` +} + +function yellow (str) { + return `\x1b[1m\x1b[33m${str}\x1b[39m\x1b[22m` +} + +module.exports = { + blue, + green, + red, + yellow, + logError +} diff --git a/build/utils/style.js b/build/utils/style.js new file mode 100644 index 0000000..6a388f8 --- /dev/null +++ b/build/utils/style.js @@ -0,0 +1,66 @@ +const path = require('path') +const postcss = require('postcss') +const cssnext = require('postcss-cssnext') +const CleanCSS = require('clean-css') +const { logError } = require('./log.js') +const write = require('./write.js') + +function processCss (style) { + const componentName = path.basename(style.id, '.vue') + return postcss([cssnext()]) + .process(style.code, {}) + .then(result => { + return { + name: componentName, + css: result.css, + map: result.map + } + }) +} + +let stylus +function processStylus (style) { + try { + stylus = stylus || require('stylus') + } catch (e) { + logError(e) + } + const componentName = path.basename(style.id, '.vue') + return new Promise((resolve, reject) => { + stylus.render(style.code, function (err, css) { + if (err) return reject(err) + resolve({ + original: { + code: style.code, + ext: 'styl' + }, + name: componentName, + css + }) + }) + }) +} + +function processStyle (style) { + if (style.lang === 'css') { + return processCss(style) + } else if (style.lang === 'stylus') { + return processStylus(style) + } else { + throw new Error(`Unknown style language '${style.lang}'`) + } +} + +function writeCss (style) { + write(`dist/${style.name}.css`, style.css) + if (style.original) { + write(`dist/${style.name}.${style.original.ext}`, style.original.code) + } + if (style.map) write(`dist/${style.name}.css.map`, style.map) + write(`dist/${style.name}.min.css`, new CleanCSS().minify(style.css).styles) +} + +module.exports = { + writeCss, + processStyle +} diff --git a/build/utils/write.js b/build/utils/write.js new file mode 100644 index 0000000..0d4a1e9 --- /dev/null +++ b/build/utils/write.js @@ -0,0 +1,19 @@ +const fs = require('fs') + +const { blue } = require('./log.js') + +function write (dest, code) { + return new Promise(function (resolve, reject) { + fs.writeFile(dest, code, function (err) { + if (err) return reject(err) + console.log(blue(dest) + ' ' + getSize(code)) + resolve(code) + }) + }) +} + +function getSize (code) { + return (code.length / 1024).toFixed(2) + 'kb' +} + +module.exports = write diff --git a/build/webpack.config.base.js b/build/webpack.config.base.js new file mode 100644 index 0000000..e5f9e36 --- /dev/null +++ b/build/webpack.config.base.js @@ -0,0 +1,59 @@ +const webpack = require('webpack') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const { resolve } = require('path') + +const { + banner, + filename, + version, + vueLoaders +} = require('./utils') + +const plugins = [ + new webpack.DefinePlugin({ + '__VERSION__': JSON.stringify(version), + 'process.env.NODE_ENV': '"test"' + }), + new webpack.BannerPlugin({ banner, raw: true, entryOnly: true }), + new ExtractTextPlugin({ + filename: `${filename}.css`, + // Don't extract css in test mode + disable: /^(common|test)$/.test(process.env.NODE_ENV) + }) +] + +module.exports = { + output: { + path: resolve(__dirname, '../dist'), + filename: `${filename}.common.js` + }, + entry: './src/index.js', + resolve: { + extensions: ['.js', '.vue', '.jsx', 'css'], + alias: { + 'src': resolve(__dirname, '../src') + } + }, + module: { + rules: [ + { + test: /.jsx?$/, + use: 'babel-loader', + include: [ + resolve(__dirname, '../node_modules/@material'), + resolve(__dirname, '../src'), + resolve(__dirname, '../test') + ] + }, + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + loaders: vueLoaders, + postcss: [require('postcss-cssnext')()] + } + } + ] + }, + plugins +} diff --git a/build/webpack.config.dev.js b/build/webpack.config.dev.js new file mode 100644 index 0000000..a644978 --- /dev/null +++ b/build/webpack.config.dev.js @@ -0,0 +1,97 @@ +const webpack = require('webpack') +const merge = require('webpack-merge') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin +const DashboardPlugin = require('webpack-dashboard/plugin') +const base = require('./webpack.config.base') +const { resolve, join } = require('path') +const { existsSync } = require('fs') +const { + dllName, + logError, + red, + vueLoaders +} = require('./utils') + +const rootDir = resolve(__dirname, '../test') +const buildPath = resolve(rootDir, 'dist') + +if (!existsSync(join(buildPath, dllName) + '.dll.js')) { + logError(red('The DLL manifest is missing. Please run `npm run build:dll` (Quit this with `q`)')) + process.exit(1) +} + +const dllManifest = require( + join(buildPath, dllName) + '.json' +) + +module.exports = merge(base, { + entry: { + tests: resolve(rootDir, 'visual.js') + }, + output: { + path: buildPath, + filename: '[name].js', + chunkFilename: '[id].js' + }, + module: { + rules: [ + { + test: /.scss$/, + loader: vueLoaders.scss, + include: [ + resolve(__dirname, '../node_modules/@material'), + resolve(__dirname, '../src') + ] + } + ] + }, + plugins: [ + new webpack.DllReferencePlugin({ + context: join(__dirname, '..'), + manifest: dllManifest + }), + new HtmlWebpackPlugin({ + chunkSortMode: 'dependency' + }), + new AddAssetHtmlPlugin({ + filepath: require.resolve( + join(buildPath, dllName) + '.dll.js' + ) + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks (module, count) { + return ( + module.resource && + /\.js$/.test(module.resource) && + module.resource.indexOf(join(__dirname, '../node_modules/')) === 0 + ) + } + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest', + chunks: ['vendor'] + }), + new DashboardPlugin(), + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + openAnalyzer: false, + reportFilename: resolve(__dirname, `../reports/${process.env.NODE_ENV}.html`) + }) + ], + devtool: '#eval-source-map', + devServer: { + inline: true, + stats: { + colors: true, + chunks: false, + cached: false + }, + contentBase: buildPath + }, + performance: { + hints: false + } +}) diff --git a/build/webpack.config.dll.js b/build/webpack.config.dll.js new file mode 100644 index 0000000..52a068f --- /dev/null +++ b/build/webpack.config.dll.js @@ -0,0 +1,28 @@ +const { resolve, join } = require('path') +const webpack = require('webpack') +const pkg = require('../package.json') + +const rootDir = resolve(__dirname, '../test') +const buildPath = resolve(rootDir, 'dist') + +const entry = {} +entry[pkg.dllPlugin.name] = pkg.dllPlugin.include + +module.exports = { + devtool: '#source-map', + entry, + output: { + path: buildPath, + filename: '[name].dll.js', + library: '[name]' + }, + plugins: [ + new webpack.DllPlugin({ + name: '[name]', + path: join(buildPath, '[name].json') + }) + ], + performance: { + hints: false + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..748d91a --- /dev/null +++ b/package.json @@ -0,0 +1,139 @@ +{ + "name": "vue-fela", + "version": "0.0.1", + "description": "Fela bindings for vue", + "author": "Dustin Hoffner", + "main": "dist/vue-fela.common.js", + "module": "dist/vue-fela.esm.js", + "browser": "dist/vue-fela.js", + "unpkg": "dist/vue-fela.js", + "style": "dist/vue-fela.css", + "files": [ + "dist", + "src" + ], + "scripts": { + "clean": "rimraf dist", + "build": "node build/build.js", + "build:dll": "webpack --progress --config build/webpack.config.dll.js", + "lint": "yon run lint:js && yon run lint:css", + "lint:js": "eslint --ext js --ext jsx --ext vue src test/**/*.spec.js test/*.js build", + "lint:js:fix": "yon run lint:js -- --fix", + "lint:css": "stylelint src/**/*.{vue,css}", + "lint:staged": "lint-staged", + "pretest": "yon run lint", + "test": "cross-env BABEL_ENV=test karma start test/karma.conf.js --single-run", + "dev": "webpack-dashboard -- webpack-dev-server --config build/webpack.config.dev.js --open", + "dev:coverage": "cross-env BABEL_ENV=test karma start test/karma.conf.js", + "prepublish": "yon run build" + }, + "lint-staged": { + "*.{vue,jsx,js}": [ + "eslint --fix" + ], + "*.{vue,css}": [ + "stylefmt", + "stylelint" + ] + }, + "pre-commit": "lint:staged", + "devDependencies": { + "add-asset-html-webpack-plugin": "^2.0.0", + "babel-core": "^6.24.0", + "babel-eslint": "^7.2.0", + "babel-helper-vue-jsx-merge-props": "^2.0.0", + "babel-loader": "^7.0.0", + "babel-plugin-istanbul": "^4.1.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-plugin-transform-vue-jsx": "^3.4.0", + "babel-preset-env": "^1.4.0", + "buble": "^0.15.2", + "chai": "^3.5.0", + "chai-dom": "^1.4.0", + "clean-css": "^4.0.0", + "cross-env": "^4.0.0", + "css-loader": "^0.28.0", + "eslint": "^3.19.0", + "eslint-config-vue": "^2.0.0", + "eslint-plugin-vue": "^2.0.0", + "extract-text-webpack-plugin": "^2.1.0", + "html-webpack-plugin": "^2.28.0", + "karma": "^1.7.0", + "karma-chai-dom": "^1.1.0", + "karma-chrome-launcher": "^2.1.0", + "karma-coverage": "^1.1.0", + "karma-mocha": "^1.3.0", + "karma-sinon-chai": "^1.3.0", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "^0.0.31", + "karma-webpack": "^2.0.0", + "lint-staged": "^3.4.0", + "mkdirp": "^0.5.1", + "mocha": "^3.3.0", + "mocha-css": "^1.0.1", + "postcss": "^6.0.0", + "postcss-cssnext": "^2.10.0", + "pre-commit": "^1.2.0", + "rimraf": "^2.6.0", + "rollup": "^0.41.6", + "rollup-plugin-buble": "^0.15.0", + "rollup-plugin-commonjs": "^8.0.0", + "rollup-plugin-jsx": "^1.0.0", + "rollup-plugin-node-resolve": "^3.0.0", + "rollup-plugin-postcss": "^0.4.1", + "rollup-plugin-replace": "^1.1.0", + "rollup-plugin-vue": "^2.3.0", + "sinon": "2.2.0", + "sinon-chai": "^2.10.0", + "style-loader": "^0.17.0", + "stylefmt": "^5.3.0", + "stylelint": "^7.10.0", + "stylelint-config-standard": "^16.0.0", + "stylelint-processor-html": "^1.0.0", + "uglify-js": "^3.0.0", + "uppercamelcase": "^3.0.0", + "vue": "^2.3.0", + "vue-loader": "^12.0.0", + "vue-template-compiler": "^2.3.0", + "webpack": "^2.5.0", + "webpack-bundle-analyzer": "^2.4.0", + "webpack-dashboard": "^0.4.0", + "webpack-dev-server": "^2.4.0", + "webpack-merge": "^4.0.0", + "yarn-or-npm": "^2.0.0" + }, + "peerDependencies": { + "vue": "^2.3.0" + }, + "dllPlugin": { + "name": "vuePluginTemplateDeps", + "include": [ + "mocha/mocha.js", + "style-loader!css-loader!mocha-css", + "html-entities", + "vue/dist/vue.js", + "chai", + "core-js/library", + "url", + "sockjs-client", + "vue-style-loader/lib/addStylesClient.js", + "events", + "ansi-html", + "style-loader/addStyles.js" + ] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dustin-h/vue-fela.git" + }, + "bugs": { + "url": "https://github.com/dustin-h/vue-fela/issues" + }, + "homepage": "https://github.com/dustin-h/vue-fela#readme", + "license": { + "type": "MIT", + "url": "http://www.opensource.org/licenses/mit-license.php" + } +} diff --git a/src/StyleProvider.vue b/src/StyleProvider.vue new file mode 100644 index 0000000..b3bc613 --- /dev/null +++ b/src/StyleProvider.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/fela.js b/src/fela.js new file mode 100644 index 0000000..8b54111 --- /dev/null +++ b/src/fela.js @@ -0,0 +1,55 @@ + +import { createRenderer } from 'fela' +import { render } from 'fela-dom' + +export const isServer = typeof window === 'undefined' +export const isClient = !isServer + +var resetRendering = true +var renderedString = null +var alreadyRenderedInClient = false +var renderer = null + +export function createRenderer(felaConfig) { + renderer = createRenderer(felaConfig) +} + +export function renderToString() { + if (isServer) { + if (resetRendering !== true) { + renderedString = renderer.renderToString() + } + resetRendering = true + return renderedString + } else { + if (alreadyRenderedInClient !== true) { + alreadyRenderedInClient = true + const mountNode = document.createElement('style') + document.head.appendChild(mountNode) + render(renderer, mountNode) + } + return '' + } + +} + +export function renderStyles(rules) { + return function render() { + if (resetRendering === true && isServer) { + renderer = createRenderer(felaConfig) + resetRendering = false + } + + var styles = {} + for (var i in rules) { + var fn = rules[i] + if (typeof rules[i] !== 'function') { + fn = () => { + return rules[i] + } + } + styles[i] = renderer.renderRule(fn.bind(this), this) + } + return styles; + }; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..704b530 --- /dev/null +++ b/src/index.js @@ -0,0 +1,13 @@ +import StyleProvider from './StyleProvider.vue' +import { renderStyles, createRenderer } from './fela.js' + +function plugin(Vue) { + Vue.component('FelaProvider', StyleProvider) + + Vue.prototype.$renderStyles = felaBinder.renderStyles +} + +export default function getPlugin(felaConfig) { + createRenderer(felaConfig) + return plugin +} diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..959a4f4 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,9 @@ +{ + "env": { + "mocha": true + }, + "globals": { + "expect": true, + "sinon": true + } +} diff --git a/test/helpers/Test.vue b/test/helpers/Test.vue new file mode 100644 index 0000000..b40fbe2 --- /dev/null +++ b/test/helpers/Test.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/test/helpers/index.js b/test/helpers/index.js new file mode 100644 index 0000000..6a37a2a --- /dev/null +++ b/test/helpers/index.js @@ -0,0 +1,51 @@ +import camelcase from 'camelcase' +import { createVM, Vue } from './utils' +import { nextTick } from './wait-for-update' + +export function dataPropagationTest (Component) { + return function () { + const spy = sinon.spy() + const vm = createVM(this, function (h) { + return ( + Hello + ) + }) + spy.should.have.not.been.called + vm.$('.custom').should.exist + vm.$('.custom').click() + spy.should.have.been.calledOnce + } +} + +export function attrTest (it, base, Component, attr) { + const attrs = Array.isArray(attr) ? attr : [attr] + + attrs.forEach(attr => { + it(attr, function (done) { + const vm = createVM(this, function (h) { + const opts = { + props: { + [camelcase(attr)]: this.active + } + } + return ( + {attr} + ) + }, { + data: { active: true } + }) + vm.$(`.${base}`).should.have.class(`${base}--${attr}`) + vm.active = false + nextTick().then(() => { + vm.$(`.${base}`).should.not.have.class(`${base}--${attr}`) + vm.active = true + }).then(done) + }) + }) +} + +export { + createVM, + Vue, + nextTick +} diff --git a/test/helpers/utils.js b/test/helpers/utils.js new file mode 100644 index 0000000..1c32b43 --- /dev/null +++ b/test/helpers/utils.js @@ -0,0 +1,89 @@ +import Vue from 'vue/dist/vue.js' +import Test from './Test.vue' + +Vue.config.productionTip = false +const isKarma = !!window.__karma__ + +export function createVM (context, template, opts = {}) { + return isKarma + ? createKarmaTest(context, template, opts) + : createVisualTest(context, template, opts) +} + +const emptyNodes = document.querySelectorAll('nonexistant') +Vue.prototype.$$ = function $$ (selector) { + const els = document.querySelectorAll(selector) + const vmEls = this.$el.querySelectorAll(selector) + const fn = vmEls.length + ? el => vmEls.find(el) + : el => this.$el === el + const found = Array.from(els).filter(fn) + return found.length + ? found + : emptyNodes +} + +Vue.prototype.$ = function $ (selector) { + const els = document.querySelectorAll(selector) + const vmEl = this.$el.querySelector(selector) + const fn = vmEl + ? el => el === vmEl + : el => el === this.$el + // Allow should chaining for tests + return Array.from(els).find(fn) || emptyNodes +} + +export function createKarmaTest (context, template, opts) { + const el = document.createElement('div') + document.getElementById('tests').appendChild(el) + const render = typeof template === 'string' + ? { template: `
${template}
` } + : { render: template } + return new Vue({ + el, + name: 'Test', + ...render, + ...opts + }) +} + +export function createVisualTest (context, template, opts) { + let vm + if (typeof template === 'string') { + opts.components = opts.components || {} + // Let the user define a test component + if (!opts.components.Test) { + opts.components.Test = Test + } + vm = new Vue({ + name: 'TestContainer', + el: context.DOMElement, + template: `${template}`, + ...opts + }) + } else { + // TODO allow redefinition of Test component + vm = new Vue({ + name: 'TestContainer', + el: context.DOMElement, + render (h) { + return h(Test, { + attrs: { + id: context.DOMElement.id + } + // render the passed component with this scope + }, [template.call(this, h)]) + }, + ...opts + }) + } + + context.DOMElement.vm = vm + return vm +} + +export function register (name, component) { + Vue.component(name, component) +} + +export { isKarma, Vue } diff --git a/test/helpers/wait-for-update.js b/test/helpers/wait-for-update.js new file mode 100644 index 0000000..fb0fe3f --- /dev/null +++ b/test/helpers/wait-for-update.js @@ -0,0 +1,56 @@ +import Vue from 'vue/dist/vue.js' + +// Testing helper +// nextTick().then(() => { +// +// Automatically waits for nextTick +// }).then(() => { +// return a promise or value to skip the wait +// }) +function nextTick () { + const jobs = [] + let done + + const chainer = { + then (cb) { + jobs.push(cb) + return chainer + } + } + + function shift (...args) { + const job = jobs.shift() + let result + try { + result = job(...args) + } catch (e) { + jobs.length = 0 + done(e) + } + + // wait for nextTick + if (result !== undefined) { + if (result.then) { + result.then(shift) + } else { + shift(result) + } + } else if (jobs.length) { + requestAnimationFrame(() => Vue.nextTick(shift)) + } + } + + // First time + Vue.nextTick(() => { + done = jobs[jobs.length - 1] + if (done.toString().slice(0, 14) !== 'function (err)') { + throw new Error('waitForUpdate chain is missing .then(done)') + } + shift() + }) + + return chainer +} + +exports.nextTick = nextTick +exports.delay = time => new Promise(resolve => setTimeout(resolve, time)) diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..a12a7ae --- /dev/null +++ b/test/index.js @@ -0,0 +1,32 @@ +// Polyfill fn.bind() for PhantomJS +import bind from 'function-bind' +/* eslint-disable no-extend-native */ +Function.prototype.bind = bind + +// Polyfill Object.assign for PhantomJS +import objectAssign from 'object-assign' +Object.assign = objectAssign + +// require all src files for coverage. +// you can also change this to match only the subset of files that +// you want coverage for. +const srcContext = require.context('../src', true, /^\.\/(?!index(\.js)?$)/) +srcContext.keys().forEach(srcContext) + +// Use a div to insert elements +before(function () { + const el = document.createElement('DIV') + el.id = 'tests' + document.body.appendChild(el) +}) + +// Remove every test html scenario +afterEach(function () { + const el = document.getElementById('tests') + for (let i = 0; i < el.children.length; ++i) { + el.removeChild(el.children[i]) + } +}) + +const specsContext = require.context('./specs', true) +specsContext.keys().forEach(specsContext) diff --git a/test/karma.conf.js b/test/karma.conf.js new file mode 100644 index 0000000..8f0871e --- /dev/null +++ b/test/karma.conf.js @@ -0,0 +1,44 @@ +const merge = require('webpack-merge') +const baseConfig = require('../build/webpack.config.dev.js') + +const webpackConfig = merge(baseConfig, { + // use inline sourcemap for karma-sourcemap-loader + devtool: '#inline-source-map' +}) + +webpackConfig.plugins = [] + +const vueRule = webpackConfig.module.rules.find(rule => rule.loader === 'vue-loader') +vueRule.options = vueRule.options || {} +vueRule.options.loaders = vueRule.options.loaders || {} +vueRule.options.loaders.js = 'babel-loader' + +// no need for app entry during tests +delete webpackConfig.entry + +module.exports = function (config) { + config.set({ + // to run in additional browsers: + // 1. install corresponding karma launcher + // http://karma-runner.github.io/0.13/config/browsers.html + // 2. add it to the `browsers` array below. + browsers: ['Chrome'], + frameworks: ['mocha', 'chai-dom', 'sinon-chai'], + reporters: ['spec', 'coverage'], + files: ['./index.js'], + preprocessors: { + './index.js': ['webpack', 'sourcemap'] + }, + webpack: webpackConfig, + webpackMiddleware: { + noInfo: true + }, + coverageReporter: { + dir: './coverage', + reporters: [ + { type: 'lcov', subdir: '.' }, + { type: 'text-summary' } + ] + } + }) +} diff --git a/test/specs/Hello-jsx.spec.js b/test/specs/Hello-jsx.spec.js new file mode 100644 index 0000000..926e797 --- /dev/null +++ b/test/specs/Hello-jsx.spec.js @@ -0,0 +1,20 @@ +import HelloJsx from 'src/Hello.jsx' +import { createVM } from '../helpers/utils.js' + +describe('Hello.jsx', function () { + it('should render correct contents', function () { + const vm = createVM(this, ` + +`, { components: { HelloJsx }}) + vm.$el.querySelector('h1').textContent.should.eql('Hello JSX') + }) + + it('renders JSX too', function () { + // You can write your tests in JSX but make sure to use the lower case + // version of your component because otherw + const vm = createVM(this, h => ( + + ), { components: { HelloJsx }}) + vm.$el.querySelector('h1').textContent.should.eql('Hello JSX') + }) +}) diff --git a/test/specs/Hello.spec.js b/test/specs/Hello.spec.js new file mode 100644 index 0000000..0e0a91e --- /dev/null +++ b/test/specs/Hello.spec.js @@ -0,0 +1,11 @@ +import Hello from 'src/Hello.vue' +import { createVM } from '../helpers/utils.js' + +describe('Hello.vue', function () { + it('should render correct contents', function () { + const vm = createVM(this, ` + +`, { components: { Hello }}) + vm.$el.querySelector('.hello h1').textContent.should.eql('Hello World!') + }) +}) diff --git a/test/visual.js b/test/visual.js new file mode 100644 index 0000000..84d276b --- /dev/null +++ b/test/visual.js @@ -0,0 +1,69 @@ +import 'style-loader!css-loader!mocha-css' + +// create a div where mocha can add its stuff +const mochaDiv = document.createElement('DIV') +mochaDiv.id = 'mocha' +document.body.appendChild(mochaDiv) + +import 'mocha/mocha.js' +import sinon from 'sinon' +import chai from 'chai' +window.mocha.setup({ + ui: 'bdd', + slow: 750, + timeout: 5000, + globals: [ + '__VUE_DEVTOOLS_INSTANCE_MAP__', + 'script', + 'inject', + 'originalOpenFunction' + ] +}) +window.sinon = sinon +chai.use(require('chai-dom')) +chai.use(require('sinon-chai')) +chai.should() + +let vms = [] +let testId = 0 + +beforeEach(function () { + this.DOMElement = document.createElement('DIV') + this.DOMElement.id = `test-${++testId}` + document.body.appendChild(this.DOMElement) +}) + +afterEach(function () { + const testReportElements = document.getElementsByClassName('test') + const lastReportElement = testReportElements[testReportElements.length - 1] + + if (!lastReportElement) return + const el = document.getElementById(this.DOMElement.id) + if (el) lastReportElement.appendChild(el) + // Save the vm to hide it later + if (this.DOMElement.vm) vms.push(this.DOMElement.vm) +}) + +// Hide all tests at the end to prevent some weird bugs +before(function () { + vms = [] + testId = 0 +}) +after(function () { + requestAnimationFrame(function () { + setTimeout(function () { + vms.forEach(vm => { + // Hide if test passed + if (!vm.$el.parentElement.classList.contains('fail')) { + vm.$children[0].visible = false + } + }) + }, 100) + }) +}) + +const specsContext = require.context('./specs', true) +specsContext.keys().forEach(specsContext) + +window.mocha.checkLeaks() +window.mocha.run()