├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── package.json ├── src ├── assets │ ├── .gitkeep │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── mstile-150x150.png ├── components │ ├── app.js │ ├── header │ │ ├── index.js │ │ └── style.less │ ├── home │ │ ├── index.js │ │ └── style.less │ └── profile │ │ ├── index.js │ │ └── style.less ├── index.html ├── index.js ├── lib │ └── react.js └── style │ ├── helpers.less │ ├── index.less │ ├── mixins.less │ └── variables.less └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,.*rc,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "es6": true 9 | }, 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "modules": true, 13 | "jsx": true 14 | } 15 | }, 16 | "globals": {}, 17 | "rules": { 18 | "no-empty": 0, 19 | "no-console": 0, 20 | "no-empty-pattern": 0, 21 | "no-unused-vars": [0, { "varsIgnorePattern": "^h$" }], 22 | "no-cond-assign": 1, 23 | "semi": 2, 24 | "camelcase": 0, 25 | "comma-style": 2, 26 | "comma-dangle": [2, "never"], 27 | "indent": [2, "tab", {"SwitchCase": 1}], 28 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 29 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 30 | "max-nested-callbacks": [2, 3], 31 | "no-eval": 2, 32 | "no-implied-eval": 2, 33 | "no-new-func": 2, 34 | "guard-for-in": 2, 35 | "eqeqeq": 1, 36 | "no-else-return": 2, 37 | "no-redeclare": 2, 38 | "no-dupe-keys": 2, 39 | "radix": 2, 40 | "strict": [2, "never"], 41 | "no-shadow": 0, 42 | "no-delete-var": 2, 43 | "no-undef-init": 2, 44 | "no-shadow-restricted-names": 2, 45 | "handle-callback-err": 0, 46 | "no-lonely-if": 2, 47 | "keyword-spacing": 2, 48 | "constructor-super": 2, 49 | "no-this-before-super": 2, 50 | "no-dupe-class-members": 2, 51 | "no-const-assign": 2, 52 | "prefer-spread": 2, 53 | "no-useless-concat": 2, 54 | "no-var": 2, 55 | "object-shorthand": 2, 56 | "prefer-arrow-callback": 2 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | /build 4 | .DS_Store 5 | /coverage 6 | /.idea 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Preact] + [React Router] v4, _without [preact-compat]_ 2 | 3 | It works! You just need to alias a couple things (PropTypes & Children). 4 | 5 | 6 | [Preact]: https://github.com/developit/preact 7 | [preact-compat]: https://github.com/developit/preact-compat 8 | [React Router]: https://github.com/reacttraining/react-router 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-boilerplate", 3 | "version": "5.0.0", 4 | "description": "Ready-to-go Preact starter project powered by webpack.", 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --progress", 7 | "start": "serve build -s -c 1", 8 | "prestart": "npm run build", 9 | "build": "cross-env NODE_ENV=production webpack -p --progress", 10 | "prebuild": "mkdirp build && ncp src/assets build/assets", 11 | "test": "npm run -s lint && npm run -s test:karma", 12 | "test:karma": "karma start test/karma.conf.js --single-run", 13 | "lint": "eslint {src,test}" 14 | }, 15 | "keywords": [ 16 | "preact", 17 | "boilerplate", 18 | "webpack" 19 | ], 20 | "license": "MIT", 21 | "author": "Jason Miller ", 22 | "devDependencies": { 23 | "autoprefixer": "^6.4.0", 24 | "babel": "^6.5.2", 25 | "babel-core": "^6.14.0", 26 | "babel-eslint": "^7.0.0", 27 | "babel-loader": "^6.2.5", 28 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 29 | "babel-plugin-transform-react-jsx": "^6.8.0", 30 | "babel-preset-es2015": "^6.14.0", 31 | "babel-preset-stage-0": "^6.5.0", 32 | "babel-register": "^6.14.0", 33 | "babel-runtime": "^6.11.6", 34 | "chai": "^3.5.0", 35 | "copy-webpack-plugin": "^4.0.1", 36 | "core-js": "^2.4.1", 37 | "cross-env": "^3.1.3", 38 | "css-loader": "^0.26.1", 39 | "eslint": "^3.0.1", 40 | "extract-text-webpack-plugin": "^1.0.1", 41 | "file-loader": "^0.9.0", 42 | "html-webpack-plugin": "^2.22.0", 43 | "isparta-loader": "^2.0.0", 44 | "json-loader": "^0.5.4", 45 | "karma": "^1.0.0", 46 | "karma-chai": "^0.1.0", 47 | "karma-chai-sinon": "^0.1.5", 48 | "karma-coverage": "^1.1.1", 49 | "karma-mocha": "^1.0.1", 50 | "karma-mocha-reporter": "^2.1.0", 51 | "karma-phantomjs-launcher": "^1.0.2", 52 | "karma-sourcemap-loader": "^0.3.7", 53 | "karma-webpack": "^1.8.0", 54 | "less": "^2.7.1", 55 | "less-loader": "^2.2.3", 56 | "mkdirp": "^0.5.1", 57 | "mocha": "^3.0.0", 58 | "ncp": "^2.0.0", 59 | "offline-plugin": "^4.5.3", 60 | "phantomjs-prebuilt": "^2.1.12", 61 | "postcss-loader": "^1.2.1", 62 | "raw-loader": "^0.5.1", 63 | "replace-bundle-webpack-plugin": "^1.0.0", 64 | "sinon": "^1.17.5", 65 | "sinon-chai": "^2.8.0", 66 | "source-map-loader": "^0.1.5", 67 | "style-loader": "^0.13.0", 68 | "url-loader": "^0.5.7", 69 | "webpack": "^1.13.2", 70 | "webpack-dev-server": "^1.15.0" 71 | }, 72 | "dependencies": { 73 | "babel-plugin-transform-react-remove-prop-types": "^0.2.11", 74 | "preact": "beta", 75 | "preact-compat": "^3.0.0", 76 | "preact-render-to-string": "^3.5.0", 77 | "preact-router": "^2.0.0", 78 | "promise-polyfill": "^6.0.2", 79 | "proptypes": "^0.14.3", 80 | "react-router": "4.0.0-beta.7", 81 | "react-router-dom": "4.0.0-beta.7", 82 | "serve": "^2.0.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/react-router-4-test/db0efd539dab962e713f0b8d611d429848c77041/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/react-router-4-test/db0efd539dab962e713f0b8d611d429848c77041/src/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/react-router-4-test/db0efd539dab962e713f0b8d611d429848c77041/src/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/react-router-4-test/db0efd539dab962e713f0b8d611d429848c77041/src/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/react-router-4-test/db0efd539dab962e713f0b8d611d429848c77041/src/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/react-router-4-test/db0efd539dab962e713f0b8d611d429848c77041/src/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developit/react-router-4-test/db0efd539dab962e713f0b8d611d429848c77041/src/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import Router from 'react-router-dom/BrowserRouter'; 3 | import Route from 'react-router/Route'; 4 | import Header from './header'; 5 | import Home from './home'; 6 | import Profile from './profile'; 7 | 8 | export default class App extends Component { 9 | render() { 10 | return ( 11 | 12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import Link from 'react-router-dom/Link'; 3 | import style from './style'; 4 | 5 | export default class Header extends Component { 6 | render() { 7 | return ( 8 |
9 |

Preact App

10 | 15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/header/style.less: -------------------------------------------------------------------------------- 1 | @import '~style/helpers'; 2 | 3 | .header { 4 | position: fixed; 5 | left: 0; 6 | top: 0; 7 | width: 100%; 8 | height: 56px; 9 | padding: 0; 10 | background: #673AB7; 11 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 12 | z-index: 50; 13 | 14 | h1 { 15 | float: left; 16 | margin: 0; 17 | padding: 0 15px; 18 | font-size: 24px; 19 | line-height: 56px; 20 | font-weight: 400; 21 | color: #FFF; 22 | } 23 | 24 | nav { 25 | float: right; 26 | font-size: 100%; 27 | 28 | a { 29 | display: inline-block; 30 | height: 56px; 31 | line-height: 56px; 32 | padding: 0 15px; 33 | min-width: 50px; 34 | text-align: center; 35 | background: rgba(255,255,255,0); 36 | text-decoration: none; 37 | color: #EEE; 38 | will-change: background-color; 39 | 40 | &:hover, &:active { 41 | background: rgba(255,255,255,0.3); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/home/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import style from './style'; 3 | 4 | export default class Home extends Component { 5 | render() { 6 | return ( 7 |
8 |

Home

9 |

This is the Home component.

10 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/home/style.less: -------------------------------------------------------------------------------- 1 | @import '~style/helpers'; 2 | 3 | .home { 4 | padding: 56px 20px; 5 | min-height: 100%; 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/profile/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import style from './style'; 3 | 4 | export default class Profile extends Component { 5 | state = { 6 | count: 0 7 | }; 8 | 9 | // gets called when this route is navigated to 10 | componentDidMount() { 11 | // start a timer for the clock: 12 | this.timer = setInterval(::this.updateTime, 1000); 13 | this.updateTime(); 14 | 15 | // every time we get remounted, increment a counter: 16 | this.setState({ count: this.state.count+1 }); 17 | } 18 | 19 | // gets called just before navigating away from the route 20 | componentWillUnmount() { 21 | clearInterval(this.timer); 22 | } 23 | 24 | // update the current time 25 | updateTime() { 26 | let time = new Date().toLocaleString(); 27 | this.setState({ time }); 28 | } 29 | 30 | // Note: `user` comes from the URL, courtesy of our router 31 | render({ user }, { time, count }) { 32 | return ( 33 |
34 |

Profile: { user }

35 |

This is the user profile for a user named { user }.

36 | 37 |
Current time: { time }
38 |
Profile route mounted { count } times.
39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/profile/style.less: -------------------------------------------------------------------------------- 1 | @import '~style/helpers'; 2 | 3 | .profile { 4 | padding: 56px 20px; 5 | min-height: 100%; 6 | width: 100%; 7 | background: #EEE; 8 | } 9 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preact + React Router v4 Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import './style'; 3 | 4 | let root; 5 | function init() { 6 | let App = require('./components/app').default; 7 | root = render(, document.body, root); 8 | } 9 | 10 | if (module.hot) module.hot.accept('./components/app', init); 11 | init(); 12 | -------------------------------------------------------------------------------- /src/lib/react.js: -------------------------------------------------------------------------------- 1 | /** This file is what React Router gets when it imports "react". 2 | * It's just regular ol' Preact, but with: 3 | * - h() aliased as createElement() 4 | * - PropTypes mixed in 5 | * - vnode.children deleted when empty 6 | */ 7 | 8 | import { h as createElement, Component, options } from 'preact'; 9 | import PropTypes from 'proptypes'; 10 | 11 | let old = options.vnode; 12 | options.vnode = vnode => { 13 | if (vnode.children && !vnode.children.length) delete vnode.children; 14 | if (old) old(vnode); 15 | }; 16 | 17 | const Children = { only: c => c[0], count: c => c.length }; 18 | 19 | export { createElement, Children, PropTypes, Component }; 20 | export default { createElement, Children, PropTypes, Component }; 21 | -------------------------------------------------------------------------------- /src/style/helpers.less: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'mixins'; 3 | -------------------------------------------------------------------------------- /src/style/index.less: -------------------------------------------------------------------------------- 1 | @import 'helpers'; 2 | 3 | html, body { 4 | height: 100%; 5 | width: 100%; 6 | padding: 0; 7 | margin: 0; 8 | background: #FAFAFA; 9 | font-family: 'Helvetica Neue', arial, sans-serif; 10 | font-weight: 400; 11 | color: #444; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | #app { 21 | height: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /src/style/mixins.less: -------------------------------------------------------------------------------- 1 | .fill() { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | .scroll() { 10 | overflow: auto; 11 | overflow-scrolling: touch; 12 | 13 | & > .inner { 14 | position: relative; 15 | transform: translateZ(0); 16 | overflow: hidden; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/style/variables.less: -------------------------------------------------------------------------------- 1 | @red: #F00; 2 | @blue: #00F; 3 | @white: #FFF; 4 | @gray: #999; 5 | @black: #000; 6 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import autoprefixer from 'autoprefixer'; 5 | import ReplacePlugin from 'replace-bundle-webpack-plugin'; 6 | import path from 'path'; 7 | 8 | const ENV = process.env.NODE_ENV || 'development'; 9 | 10 | const CSS_MAPS = ENV!=='production'; 11 | 12 | module.exports = { 13 | context: path.resolve(__dirname, "src"), 14 | entry: './index.js', 15 | 16 | output: { 17 | path: path.resolve(__dirname, "build"), 18 | publicPath: '/', 19 | filename: 'bundle.js' 20 | }, 21 | 22 | resolve: { 23 | extensions: ['', '.jsx', '.js', '.json', '.less'], 24 | modulesDirectories: [ 25 | path.resolve(__dirname, "src/lib"), 26 | path.resolve(__dirname, "node_modules"), 27 | 'node_modules' 28 | ], 29 | alias: { 30 | components: path.resolve(__dirname, "src/components"), // used for tests 31 | style: path.resolve(__dirname, "src/style") 32 | } 33 | }, 34 | 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.jsx?$/, 39 | // exclude: /node_modules/, 40 | loader: 'babel-loader', 41 | query: { 42 | babelrc: false, 43 | presets: [ 44 | ['es2015', { loose:true }], 45 | 'stage-0' 46 | ], 47 | plugins: [ 48 | 'transform-react-remove-prop-types', 49 | ['transform-react-jsx', { 'pragma': 'h' }] 50 | ] 51 | } 52 | }, 53 | { 54 | // Transform our own .(less|css) files with PostCSS and CSS-modules 55 | test: /\.(less|css)$/, 56 | include: [path.resolve(__dirname, 'src/components')], 57 | loader: ExtractTextPlugin.extract('style-loader', [ 58 | `css-loader?modules&importLoaders=1&sourceMap=${CSS_MAPS}`, 59 | 'postcss-loader', 60 | `less-loader?sourceMap=${CSS_MAPS}` 61 | ].join('!')) 62 | }, 63 | { 64 | test: /\.(less|css)$/, 65 | exclude: [path.resolve(__dirname, 'src/components')], 66 | loader: ExtractTextPlugin.extract('style-loader', [ 67 | `css-loader?sourceMap=${CSS_MAPS}`, 68 | `postcss-loader`, 69 | `less-loader?sourceMap=${CSS_MAPS}` 70 | ].join('!')) 71 | }, 72 | { 73 | test: /\.json$/, 74 | loader: 'json-loader' 75 | }, 76 | { 77 | test: /\.(xml|html|txt|md)$/, 78 | loader: 'raw-loader' 79 | }, 80 | { 81 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif)(\?.*)?$/i, 82 | loader: ENV==='production' ? 'file-loader' : 'url-loader' 83 | } 84 | ] 85 | }, 86 | 87 | postcss: () => [ 88 | autoprefixer({ browsers: 'last 2 versions' }) 89 | ], 90 | 91 | plugins: ([ 92 | new webpack.NoErrorsPlugin(), 93 | new ExtractTextPlugin('style.css', { 94 | allChunks: true, 95 | disable: ENV!=='production' 96 | }), 97 | new webpack.DefinePlugin({ 98 | 'process.env.NODE_ENV': JSON.stringify(ENV) 99 | }), 100 | new HtmlWebpackPlugin({ 101 | template: './index.html', 102 | minify: { collapseWhitespace: true } 103 | }) 104 | ]).concat( ENV==='production' ? [ 105 | new ReplacePlugin([ 106 | { 107 | partten: /throw\s+(new\s+)?[a-zA-Z]+Error\s*\(/g, 108 | replacement: (s) => 'throw 0;'+s 109 | }, 110 | { 111 | partten: /\binvariant\s\(/g, 112 | replacement: () => '(' 113 | } 114 | ]), 115 | new webpack.optimize.UglifyJsPlugin({ 116 | compress: { 117 | unsafe: true, 118 | collapse_vars: true, 119 | pure_getters: true, 120 | pure_funcs: [ 121 | 'classCallCheck', 122 | '_possibleConstructorReturn', 123 | '_classCallCheck', 124 | 'Object.freeze', 125 | 'invariant', 126 | 'warning' 127 | ] 128 | } 129 | }) 130 | ] : []), 131 | 132 | stats: { colors: true }, 133 | 134 | node: { 135 | global: true, 136 | process: false, 137 | Buffer: false, 138 | __filename: false, 139 | __dirname: false, 140 | setImmediate: false 141 | }, 142 | 143 | devtool: ENV==='production' ? 'source-map' : 'inline-source-map', 144 | 145 | devServer: { 146 | port: process.env.PORT || 8080, 147 | host: 'localhost', 148 | colors: true, 149 | publicPath: '/', 150 | contentBase: './src', 151 | historyApiFallback: true 152 | } 153 | }; 154 | --------------------------------------------------------------------------------