├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app ├── ReamError.js ├── client-entry.js ├── create-app.js ├── create-data-store.js ├── enhance-app-template.js ├── entry-template.js ├── polyfills.js ├── server-entry.js ├── server-helpers.js └── utils.js ├── bin └── cli.js ├── circle.yml ├── examples ├── custom-root-component │ ├── README.md │ ├── Root.vue │ ├── index.js │ ├── index.test.js │ └── package.json ├── custom-server-express │ ├── README.md │ ├── index.js │ ├── package.json │ ├── ream.config.js │ └── server.js ├── custom-server-http │ ├── README.md │ ├── index.js │ ├── package.json │ ├── ream.config.js │ └── server.js ├── fs-routes │ ├── README.md │ ├── index.test.js │ ├── package.json │ ├── pages │ │ ├── index.vue │ │ ├── user.vue │ │ └── user │ │ │ ├── [user_id].vue │ │ │ ├── [user_id] │ │ │ ├── friends.vue │ │ │ └── index.vue │ │ │ └── index.vue │ └── ream.config.js ├── prepopulate-vuex │ ├── README.md │ ├── index.js │ ├── index.test.js │ └── package.json ├── serve-public-files │ ├── README.md │ ├── index.js │ ├── package.json │ └── public │ │ └── style.css ├── with-apollo │ ├── createApolloClient.js │ ├── index.js │ ├── package.json │ ├── router.js │ └── views │ │ ├── index.vue │ │ └── post.vue └── with-auth │ ├── README.md │ ├── index.js │ ├── lib │ └── getToken.js │ ├── package.json │ └── views │ ├── Home.vue │ ├── Login.vue │ └── Secret.vue ├── lib ├── babel │ ├── addInitialDataKey.js │ ├── addInitialDataKey.test.js │ └── preset.js ├── emoji.js ├── hooks.js ├── index.d.ts ├── index.js ├── logger.js ├── plugins │ ├── apollo │ │ ├── apollo-inject.js │ │ └── index.js │ ├── base.js │ ├── fs-routes │ │ ├── __test__ │ │ │ ├── presets │ │ │ │ ├── empty │ │ │ │ │ └── .keep │ │ │ │ ├── ignore-garbage │ │ │ │ │ ├── garbage.txt │ │ │ │ │ └── page.vue │ │ │ │ ├── nested-children │ │ │ │ │ ├── foo.vue │ │ │ │ │ └── foo │ │ │ │ │ │ ├── bar.vue │ │ │ │ │ │ └── bar │ │ │ │ │ │ ├── baz.vue │ │ │ │ │ │ └── baz │ │ │ │ │ │ └── qux.vue │ │ │ │ ├── nested │ │ │ │ │ └── foo │ │ │ │ │ │ └── bar │ │ │ │ │ │ └── baz │ │ │ │ │ │ └── qux.vue │ │ │ │ └── typical │ │ │ │ │ ├── foo.vue │ │ │ │ │ ├── index.vue │ │ │ │ │ ├── user.vue │ │ │ │ │ └── user │ │ │ │ │ ├── [user].vue │ │ │ │ │ └── [user] │ │ │ │ │ ├── friends.vue │ │ │ │ │ └── index.vue │ │ │ ├── routes.test.js │ │ │ └── special │ │ │ │ ├── custom-base-path │ │ │ │ ├── _expected_routes.json │ │ │ │ └── foo.vue │ │ │ │ └── typescript │ │ │ │ ├── _expected_routes.json │ │ │ │ ├── index.vue │ │ │ │ └── page.ts │ │ ├── index.js │ │ ├── inject.js │ │ ├── routes-template.js │ │ └── write-routes.js │ ├── pwa │ │ ├── index.js │ │ ├── noop-sw-middleware.js │ │ ├── noop-sw.js │ │ └── pwa-inject.js │ └── vuex │ │ ├── index.js │ │ └── inject.js ├── utils │ ├── dir.js │ ├── document.js │ ├── inspect.js │ ├── loadConfig.js │ ├── minifyHtml.js │ ├── prettyPath.js │ ├── renderHtml.js │ ├── serveStatic.js │ └── setupWebpackMiddlewares.js ├── validateConfig.js ├── validateConfig.test.js └── webpack │ ├── loaders │ └── ream-babel-loader.js │ ├── plugins │ └── WatchMissingNodeModulesPlugin.js │ ├── webpack.config.base.js │ ├── webpack.config.client.js │ └── webpack.config.server.js ├── package.json ├── tap-snapshots ├── examples-custom-root-component-index.test.js-TAP.test.js ├── examples-fs-routes-index.test.js-TAP.test.js ├── examples-prepopulate-vuex-index.test.js-TAP.test.js ├── lib-plugins-fs-routes-__test__-routes.test.js-TAP.test.js ├── test-projects-errors-index.test.js-TAP.test.js └── test-projects-html-minifier-index.test.js-TAP.test.js ├── test ├── lib │ └── testProject.js └── projects │ ├── entry-middleware │ ├── index.js │ ├── index.test.js │ ├── pages │ │ └── index.vue │ └── ream.config.js │ ├── errors │ ├── index.test.js │ ├── pages │ │ ├── error.vue │ │ └── index.vue │ └── ream.config.js │ ├── html-minifier │ ├── index.test.js │ ├── pages │ │ └── index.vue │ └── ream.config.js │ └── redirect │ ├── index.test.js │ ├── pages │ ├── 1.vue │ ├── 2.vue │ └── 3.vue │ └── ream.config.js ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples/*/yarn.lock 3 | .ream 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) egoist <0x142857@gmail.com> (https://github.com/egoist) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This project is deprecated in favor of https://vapperjs.org 2 | 3 |

4 | 5 |

6 | 7 |

NPM version NPM downloads CircleCI
donate chat

8 | 9 | ## Install 10 | 11 | ```bash 12 | yarn add ream 13 | ``` 14 | 15 | ## Usage 16 | 17 | Unlike a regular Vue SPA, you must export a function which returns an object in your app entry in order to make it work with Ream: 18 | 19 | ```js 20 | // index.js 21 | import Vue from 'vue' 22 | import Router from 'vue-router' 23 | 24 | Vue.use(Router) 25 | 26 | export default () => ({ 27 | router: new Router({ 28 | mode: 'history', 29 | routes: [{ 30 | path: '/', 31 | // Dynamically load your index component 32 | component: () => import('./index.vue') 33 | }] 34 | }) 35 | }) 36 | ``` 37 | 38 | And that's it, run `ream dev` and have fun playing with your app at `http://localhost:4000`. 39 | 40 | ## Roadmap 41 | 42 | - [ ] Document how to modify internal webpack config. 43 | - [ ] Add proper tests. 44 | 45 | To make things happen faster, you may consider becoming a patron to support the development: 46 | 47 | 48 | 49 | 50 | 51 | ## Contributing 52 | 53 | 1. Fork it! 54 | 2. Create your feature branch: `git checkout -b my-new-feature` 55 | 3. Commit your changes: `git commit -am 'Add some feature'` 56 | 4. Push to the branch: `git push origin my-new-feature` 57 | 5. Submit a pull request :D 58 | 59 | 60 | ## Author 61 | 62 | **ream** © [egoist](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
63 | Authored and maintained by egoist with help from contributors ([list](https://github.com/ream/ream/contributors)). 64 | 65 | > [github.com/egoist](https://github.com/egoist) · GitHub [@egoist](https://github.com/egoist) · Twitter [@_egoistlily](https://twitter.com/_egoistlily) 66 | -------------------------------------------------------------------------------- /app/ReamError.js: -------------------------------------------------------------------------------- 1 | const captureStack = 2 | Error.captureStackTrace || 3 | function(error) { 4 | const container = new Error() 5 | 6 | Object.defineProperty(error, 'stack', { 7 | configurable: true, 8 | get() { 9 | const { stack } = container 10 | 11 | Object.defineProperty(this, 'stack', { 12 | value: stack 13 | }) 14 | 15 | return stack 16 | } 17 | }) 18 | } 19 | 20 | function inherits(ctor, superCtor) { 21 | ctor.super_ = superCtor 22 | ctor.prototype = Object.create(superCtor.prototype, { 23 | constructor: { 24 | value: ctor, 25 | enumerable: false, 26 | writable: true, 27 | configurable: true 28 | } 29 | }) 30 | } 31 | 32 | function ReamError(data) { 33 | Object.defineProperty(this, 'name', { 34 | configurable: true, 35 | value: 'ReamError', 36 | writable: true 37 | }) 38 | for (const key of Object.keys(data)) { 39 | this[key] = data[key] 40 | } 41 | inherits(this, Error) 42 | captureStack(this) 43 | } 44 | 45 | export default ReamError 46 | 47 | // Following code only works without babel 48 | // Not sure why :( 49 | // export default class ReamError extends Error { 50 | // constructor({ message, code }) { 51 | // super(message) 52 | // this.name = 'ReamError' 53 | // this.code = code 54 | // if (Error.captureStackTrace) { 55 | // Error.captureStackTrace(this, this.constructor) 56 | // } 57 | // } 58 | // } 59 | -------------------------------------------------------------------------------- /app/client-entry.js: -------------------------------------------------------------------------------- 1 | /* globals window */ 2 | import Vue from 'vue' 3 | // eslint-disable-next-line import/no-unassigned-import 4 | import './polyfills' 5 | // eslint-disable-next-line import/no-unresolved 6 | import createApp from '#app/create-app' 7 | import { routerReady, pageNotFound, runMiddlewares } from './utils' 8 | import serverHelpers from './server-helpers' 9 | import ReamError from './ReamError' 10 | 11 | const globalState = window.__REAM__ 12 | 13 | const { app, router, event, dataStore, middlewares } = createApp({ 14 | globalState 15 | }) 16 | 17 | const updateDataStore = (id, data) => { 18 | dataStore.setData(id, data) 19 | } 20 | 21 | const handleError = err => { 22 | if (err instanceof ReamError) { 23 | app.setError(err) 24 | return true 25 | } 26 | console.error(err) 27 | return false 28 | } 29 | 30 | const handleRouteGuardError = (err, next) => { 31 | if (err instanceof ReamError && err.code === 'REDIRECT') { 32 | const url = err.redirectURL 33 | if (/^(\w+:)?\/\//.test(url)) { 34 | window.location.assign(url) 35 | next(false) 36 | } else { 37 | next(url) 38 | } 39 | } else if (handleError(err)) { 40 | next() 41 | } else { 42 | next(false) 43 | } 44 | } 45 | 46 | router.beforeResolve(async (to, from, next) => { 47 | // Skip initial load on client-side 48 | if (!app.$clientRendered) return next() 49 | 50 | const matched = router.getMatchedComponents(to) 51 | if (matched.length === 0) { 52 | app.setError(pageNotFound(to.path)) 53 | return next() 54 | } 55 | 56 | const prevMatched = router.getMatchedComponents(from) 57 | let diffed = false 58 | const activated = matched.filter((c, i) => { 59 | if (diffed) return diffed 60 | diffed = prevMatched[i] !== c 61 | return diffed 62 | }) 63 | 64 | const components = activated 65 | .map(c => (typeof c === 'function' ? c.options : c)) 66 | .filter(c => c.getInitialData) 67 | 68 | try { 69 | const ctx = { router, route: to, ...serverHelpers } 70 | await runMiddlewares(middlewares, ctx) 71 | await Promise.all( 72 | components.map(async c => { 73 | const data = await c.getInitialData(ctx) 74 | updateDataStore(c.initialDataKey, data) 75 | }) 76 | ) 77 | next() 78 | } catch (err) { 79 | err.errorPath = to.path 80 | handleRouteGuardError(err, next) 81 | } 82 | }) 83 | 84 | // A global mixin that calls `getInitialData` when a route component's params change 85 | Vue.mixin({ 86 | async beforeRouteUpdate(to, from, next) { 87 | try { 88 | const context = { router, route: to } 89 | await runMiddlewares(middlewares, context) 90 | 91 | const { getInitialData } = this.$options 92 | if (getInitialData) { 93 | const data = await getInitialData(context) 94 | updateDataStore(this.$initialDataKey, data) 95 | Object.assign(this, data) 96 | } 97 | next() 98 | } catch (err) { 99 | err.url = to.path 100 | handleRouteGuardError(err, next) 101 | } 102 | } 103 | }) 104 | 105 | // Wait until router has resolved all async before hooks 106 | // and async components... 107 | async function main() { 108 | event.$emit('before-client-render') 109 | 110 | if (globalState.initialData) { 111 | dataStore.replaceState(globalState.initialData) 112 | } 113 | 114 | await routerReady(router) 115 | 116 | if (router.getMatchedComponents().length === 0) { 117 | throw new ReamError(pageNotFound(router.currentRoute.path)) 118 | } 119 | } 120 | 121 | const mountApp = app => { 122 | app.$mount('#_ream') 123 | app.$clientRendered = true 124 | } 125 | 126 | main() 127 | // eslint-disable-next-line promise/prefer-await-to-then 128 | .then(() => { 129 | mountApp(app) 130 | }) 131 | .catch(err => { 132 | if (handleError(err)) { 133 | mountApp(app) 134 | } 135 | }) 136 | -------------------------------------------------------------------------------- /app/create-app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Meta from 'vue-meta' 3 | import Router from 'vue-router' 4 | // eslint-disable-next-line import/no-unresolved 5 | import _entry from '#out/entry' 6 | // eslint-disable-next-line import/no-unresolved 7 | import enhanceApp from '#out/enhance-app' 8 | import createDataStore from './create-data-store' 9 | import { setInitialData } from './utils' 10 | 11 | Vue.config.productionTip = false 12 | 13 | Vue.use(Router) 14 | 15 | Vue.use(Meta, { 16 | keyName: 'head', 17 | attribute: 'data-ream-head', 18 | ssrAttribute: 'data-ream-ssr', 19 | tagIDKeyName: 'rhid' 20 | }) 21 | 22 | function isRouteComponent(matched, current) { 23 | for (const m of matched) { 24 | for (const key of Object.keys(m.instances)) { 25 | const instance = m.instances[key] 26 | if (instance === current) { 27 | return true 28 | } 29 | } 30 | } 31 | return false 32 | } 33 | 34 | Vue.mixin({ 35 | beforeCreate() { 36 | if (!this.$root.$options._isReamRoot) return 37 | 38 | this.$ream = this.$root 39 | this.$dataStore = this.$ream.$options.dataStore 40 | this.$isRouteComponent = isRouteComponent(this.$route.matched, this) 41 | 42 | setInitialData(this) 43 | }, 44 | data() { 45 | return Object.assign({}, this.$initialData) 46 | } 47 | }) 48 | 49 | const Root = { 50 | name: 'ReamRoot', 51 | render(h) { 52 | return h('router-view') 53 | } 54 | } 55 | 56 | const Error = { 57 | name: 'ReamError', 58 | functional: true, 59 | props: ['error'], 60 | render( 61 | h, 62 | { 63 | props: { error } 64 | } 65 | ) { 66 | return ( 67 |
68 |

69 | {error.code}: {error.message} 70 |

71 | {__DEV__ && 72 | error.code === 404 && 73 | error.errorPath === '/' && ( 74 |

You must create pages/*.vue or export "router" in entry file!

75 | )} 76 |
77 | ) 78 | } 79 | } 80 | 81 | function createRootComponent(entry) { 82 | const { root = Root, error = Error } = entry 83 | 84 | return { 85 | dataStore: createDataStore(), 86 | data() { 87 | return { 88 | error: null 89 | } 90 | }, 91 | render(h) { 92 | return h( 93 | 'div', 94 | { 95 | attrs: { 96 | id: '_ream' 97 | } 98 | }, 99 | [ 100 | this.actualError 101 | ? h(error, { 102 | props: { 103 | error: this.actualError 104 | } 105 | }) 106 | : h(root) 107 | ] 108 | ) 109 | }, 110 | methods: { 111 | setError(error) { 112 | this.error = error 113 | } 114 | }, 115 | computed: { 116 | actualError() { 117 | const error = this.error 118 | if (error && error.errorPath) { 119 | return error.errorPath === this.$route.path ? error : null 120 | } 121 | return error 122 | } 123 | } 124 | } 125 | } 126 | 127 | export default function createApp(context) { 128 | if (__DEV__ && typeof _entry !== 'function') { 129 | throw new TypeError( 130 | `The entry file should export a function but got "${typeof _entry}"` 131 | ) 132 | } 133 | 134 | const entry = _entry(context) 135 | if (__DEV__ && typeof entry !== 'object') { 136 | throw new TypeError( 137 | `The return value of the default export in entry file should be a plain object but got "${typeof entry}"` 138 | ) 139 | } 140 | 141 | const rootOptions = { 142 | ...createRootComponent(entry), 143 | _isReamRoot: true, 144 | router: entry.router || new Router({ mode: 'history' }) 145 | } 146 | const middlewares = [] 147 | const event = new Vue() 148 | const enhanceContext = { 149 | ...context, 150 | entry, 151 | rootOptions, 152 | event, 153 | addMiddleware(fn) { 154 | middlewares.push(fn) 155 | } 156 | } 157 | 158 | enhanceApp(enhanceContext) 159 | 160 | if (entry.enhanceApp) { 161 | entry.enhanceApp(enhanceContext) 162 | } 163 | if (entry.extendRootOptions) { 164 | entry.extendRootOptions(rootOptions) 165 | } 166 | if (entry.middleware) { 167 | middlewares.push(entry.middleware) 168 | } 169 | 170 | const app = new Vue(rootOptions) 171 | 172 | return { 173 | app, 174 | entry, 175 | event, 176 | middlewares, 177 | router: rootOptions.router, 178 | dataStore: rootOptions.dataStore 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /app/create-data-store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default () => 4 | new Vue({ 5 | data: { 6 | state: {} 7 | }, 8 | methods: { 9 | setData(id, data) { 10 | this.state[id] = data 11 | }, 12 | getData(id) { 13 | return this.state[id] 14 | }, 15 | replaceState(state) { 16 | this.state = state 17 | }, 18 | getState() { 19 | return this.state 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /app/enhance-app-template.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const slash = input => { 4 | const isExtendedLengthPath = /^\\\\\?\\/.test(input) 5 | const hasNonAscii = /[^\u0000-\u0080]+/.test(input) // eslint-disable-line no-control-regex 6 | 7 | if (isExtendedLengthPath || hasNonAscii) { 8 | return input 9 | } 10 | 11 | return input.replace(/\\/g, '/') 12 | } 13 | 14 | const pathToId = file => { 15 | return path.basename(slash(file)).replace(/\W/g, '_') 16 | } 17 | 18 | module.exports = api => { 19 | const enhanceAppFiles = [...api.enhanceAppFiles].map((filepath, index) => ({ 20 | id: `${pathToId(filepath)}_${index}`, 21 | filepath: slash(filepath) 22 | })) 23 | 24 | return ` 25 | import { getRequireDefault } from '#app/utils' 26 | 27 | ${[...enhanceAppFiles] 28 | .map(file => 29 | ` 30 | const ${file.id} = getRequireDefault(require('${file.filepath}')) 31 | `.trim() 32 | ) 33 | .join('\n')} 34 | 35 | export default function enhanceApp (enhanceContext) { 36 | ${[...enhanceAppFiles] 37 | .map(file => 38 | ` 39 | if (typeof ${file.id} === 'function') { 40 | ${file.id}(enhanceContext) 41 | } 42 | `.trim() 43 | ) 44 | .join('\n')} 45 | } 46 | ` 47 | } 48 | -------------------------------------------------------------------------------- /app/entry-template.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | // Poor man hot reload template for the app entry. 4 | // Ideally, this fallback should be handled by webpack. 5 | // However, at the moment there is no way to disable warning emitted by 6 | // require.resolve('#app-entry') when the entry module does not exist. 7 | // 8 | // See discussion in https://github.com/ream/ream/pull/96 9 | 10 | module.exports = api => { 11 | if ( 12 | !( 13 | api.config.entry && 14 | fs.pathExistsSync(api.resolveBaseDir(api.config.entry)) 15 | ) 16 | ) { 17 | return `export default () => ({})` 18 | } 19 | 20 | return ` 21 | import entry from '#app-entry' 22 | export default entry 23 | ` 24 | } 25 | -------------------------------------------------------------------------------- /app/polyfills.js: -------------------------------------------------------------------------------- 1 | /* globals window, Object */ 2 | window.Promise = window.Promise || require('promise-polyfill') 3 | 4 | Object.assign = require('object-assign') 5 | -------------------------------------------------------------------------------- /app/server-entry.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import createApp from '#app/create-app' 3 | import ReamError from './ReamError' 4 | import { routerReady, pageNotFound, runMiddlewares } from './utils' 5 | import serverHelpers from './server-helpers' 6 | 7 | // This exported function will be called by `bundleRenderer`. 8 | // This is where we perform data-prefetching to determine the 9 | // state of our application before actually rendering it. 10 | // Since data fetching is async, this function is expected to 11 | // return a Promise that resolves to the app instance. 12 | export default async context => { 13 | context.globalState = {} 14 | const { req, res } = context 15 | const { app, dataStore, router, entry, event, middlewares } = createApp( 16 | context 17 | ) 18 | 19 | router.push(req.url) 20 | 21 | await routerReady(router) 22 | 23 | const matchedComponents = router.getMatchedComponents() 24 | 25 | // No matched routes 26 | if (matchedComponents.length === 0) { 27 | if (res) { 28 | res.statusCode = 404 29 | app.setError(pageNotFound(req.url)) 30 | } else { 31 | throw new ReamError({ 32 | code: 'NOT_FOUND', 33 | message: `Cannot find corresponding route component for ${req.url}` 34 | }) 35 | } 36 | } 37 | 38 | const dataContext = { 39 | req, 40 | res, 41 | router, 42 | route: router.currentRoute, 43 | ...serverHelpers 44 | } 45 | await runMiddlewares(middlewares, dataContext) 46 | 47 | await Promise.all( 48 | matchedComponents.map(async Component => { 49 | if (typeof Component === 'function') { 50 | // Component created with Vue.extend 51 | Component = Component.options 52 | } 53 | const { getInitialData } = Component 54 | if (!getInitialData) return 55 | const initialData = await getInitialData(dataContext) 56 | if (initialData) { 57 | for (const key of Object.keys(initialData)) { 58 | // `undefined` value will be removed when `JSON.stringify` so we set it to `null` here 59 | if (initialData[key] === undefined) { 60 | initialData[key] = null 61 | } 62 | } 63 | } 64 | dataStore.setData(Component.initialDataKey, initialData) 65 | }) 66 | ) 67 | 68 | context.document = entry.document 69 | if (entry.getDocumentData) { 70 | const documentData = await entry.getDocumentData(dataContext) 71 | context.documentData = Object.assign({}, documentData) 72 | } 73 | 74 | if (app.$meta) { 75 | context.meta = app.$meta() 76 | } 77 | 78 | context.globalState.initialData = dataStore.getState() 79 | 80 | event.$emit('before-server-render') 81 | 82 | return app 83 | } 84 | -------------------------------------------------------------------------------- /app/server-helpers.js: -------------------------------------------------------------------------------- 1 | import ReamError from './ReamError' 2 | 3 | export default { 4 | error: err => { 5 | throw new ReamError(err) 6 | }, 7 | redirect: url => { 8 | throw new ReamError({ 9 | code: 'REDIRECT', 10 | // No error path since we don't really display this error 11 | errorPath: null, 12 | redirectURL: url 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | export const routerReady = router => 2 | new Promise((resolve, reject) => { 3 | router.onReady(resolve, reject) 4 | }) 5 | 6 | export const getRequireDefault = obj => { 7 | // eslint-disable-next-line no-prototype-builtins 8 | return obj && obj.hasOwnProperty('default') ? obj.default : obj 9 | } 10 | 11 | export const setInitialData = vm => { 12 | const { getInitialData, initialDataKey } = vm.$options 13 | 14 | if (getInitialData) { 15 | vm.$initialDataKey = vm.$initialDataKey || initialDataKey 16 | 17 | if (!vm.$initialDataKey) { 18 | throw new Error( 19 | 'Route component requires a unique `initialDataKey` to use `getInitialData` method' 20 | ) 21 | } 22 | 23 | if (!vm.$isRouteComponent) { 24 | throw new Error( 25 | '`getInitialData` method can only be used in a route component' 26 | ) 27 | } 28 | 29 | const initialData = vm.$dataStore.getData(vm.$initialDataKey) 30 | vm.$initialData = initialData 31 | } 32 | } 33 | 34 | export const pageNotFound = url => ({ 35 | code: 404, 36 | url, 37 | errorPath: url, 38 | message: 'page not found' 39 | }) 40 | 41 | export const runMiddlewares = async (middlewares, ctx) => { 42 | if (middlewares.length > 0) { 43 | for (const middleware of middlewares) { 44 | // eslint-disable-next-line no-await-in-loop 45 | await middleware(ctx) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const cac = require('cac') 3 | const chalk = require('chalk') 4 | const merge = require('lodash.merge') 5 | const logger = require('../lib/logger') 6 | 7 | const major = parseInt(process.versions.node.split('.')[0], 10) 8 | if (major < 8) { 9 | // eslint-disable-next-line import/no-unassigned-import 10 | require('async-to-gen/register') 11 | } 12 | 13 | const cli = cac() 14 | 15 | const logUrl = app => { 16 | let ip 17 | app.on('renderer-ready', () => { 18 | const { host, port } = app.config.server 19 | const isDefaultRoute = host === '0.0.0.0' 20 | logger.log( 21 | `\n App running at:\n\n - Local: ${chalk.bold( 22 | `http://${isDefaultRoute ? 'localhost' : host}:${port}` 23 | )}\n${ 24 | isDefaultRoute 25 | ? chalk.dim( 26 | ` - Network: http://${ip || 27 | (ip = require('internal-ip').v4.sync())}:${port}` 28 | ) 29 | : '' 30 | }` 31 | ) 32 | }) 33 | } 34 | 35 | const getOptions = (command, input, flags) => { 36 | const dev = command === 'dev' 37 | const options = Object.assign({}, flags, { 38 | dev, 39 | baseDir: input[0] 40 | }) 41 | const config = {} 42 | if (command === 'dev' || command === 'start') { 43 | if (flags.host) { 44 | config.server = merge(config.server, { host: flags.host }) 45 | } 46 | if (flags.port) { 47 | config.server = merge(config.server, { port: flags.port }) 48 | } 49 | } 50 | return { 51 | options, 52 | config 53 | } 54 | } 55 | 56 | cli.command('build', 'Build your application', (input, flags) => { 57 | const { options, config } = getOptions('build', input, flags) 58 | const app = require('../lib')(options, config) 59 | return app.build() 60 | }) 61 | 62 | cli 63 | .command('dev', 'Develop your application', (input, flags) => { 64 | const { options, config } = getOptions('dev', input, flags) 65 | const app = require('../lib')(options, config) 66 | logUrl(app) 67 | return app.start() 68 | }) 69 | .option('host', 'Server host (default: 0.0.0.0)') 70 | .option('port', 'Server port (default: 4000)') 71 | 72 | cli 73 | .command('start', 'Start your application in production', (input, flags) => { 74 | const { options, config } = getOptions('start', input, flags) 75 | const app = require('../lib')(options, config) 76 | logUrl(app) 77 | return app.start() 78 | }) 79 | .option('host', 'Server host (default: 0.0.0.0)') 80 | .option('port', 'Server port (default: 4000)') 81 | 82 | cli.command('generate', 'Generate static html files', (input, flags) => { 83 | const { options, config } = getOptions('generate', input, flags) 84 | const app = require('../lib')(options, config) 85 | return app.generate() 86 | }) 87 | 88 | cli 89 | .option('config', { 90 | alias: 'c', 91 | desc: 'Load a config file (default: ream.config.js)' 92 | }) 93 | .option('debug', 'Print debug logs') 94 | .option('inspectWebpack', 'Print webpack config as string') 95 | .option('progress', 'Toggle progress bar') 96 | 97 | cli.parse() 98 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:latest-browsers 6 | branches: 7 | ignore: 8 | - gh-pages # list of branches to ignore 9 | - /release\/.*/ # or ignore regexes 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | key: dependency-cache-{{ checksum "yarn.lock" }} 14 | - run: 15 | name: Install dependences 16 | command: yarn 17 | - save_cache: 18 | key: dependency-cache-{{ checksum "yarn.lock" }} 19 | paths: 20 | - ./node_modules 21 | - run: 22 | name: Test 23 | command: yarn test 24 | - run: 25 | name: Release 26 | command: if [[ "$CIRCLE_BRANCH" == "master" ]]; then yarn semantic-release; fi 27 | -------------------------------------------------------------------------------- /examples/custom-root-component/README.md: -------------------------------------------------------------------------------- 1 | # Custom root component 2 | 3 | ## How to use 4 | 5 | ```bash 6 | git clone https://github.com/ream/ream.git 7 | ``` 8 | 9 | Install dependencies: 10 | 11 | ```bash 12 | cd examples/custom-root-component 13 | yarn 14 | ``` 15 | 16 | Run it: 17 | 18 | ```bash 19 | # Start development server 20 | yarn dev 21 | 22 | # Start production server 23 | yarn build && yarn start 24 | ``` 25 | 26 | ## The idea behind this example 27 | 28 | [Use a custom root component](https://ream.js.org/entry-file.html#root). 29 | -------------------------------------------------------------------------------- /examples/custom-root-component/Root.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /examples/custom-root-component/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default () => ({ 7 | root: () => import('./Root.vue'), 8 | router: new Router({ 9 | mode: 'history', 10 | routes: [ 11 | { 12 | path: '/', 13 | component: { 14 | render(h) { 15 | return h('h1', 'homepage') 16 | } 17 | } 18 | } 19 | ] 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/custom-root-component/index.test.js: -------------------------------------------------------------------------------- 1 | const testProject = require('../../test/lib/testProject') 2 | 3 | testProject(__dirname, async (t, c) => { 4 | t.matchSnapshot((await c.axios.get('/')).data, 'page') 5 | }) 6 | -------------------------------------------------------------------------------- /examples/custom-root-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "custom-root-component", 4 | "scripts": { 5 | "dev": "ream dev", 6 | "start": "ream start", 7 | "build": "ream build" 8 | }, 9 | "dependencies": { 10 | "ream": "latest" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/custom-server-express/README.md: -------------------------------------------------------------------------------- 1 | # Custom server with `express` 2 | 3 | ## How to use 4 | 5 | ```bash 6 | git clone https://github.com/ream/ream.git 7 | ``` 8 | 9 | Install dependencies: 10 | 11 | ```bash 12 | cd examples/custom-server-express 13 | yarn 14 | ``` 15 | 16 | Run it: 17 | 18 | ```bash 19 | # Start development server 20 | yarn dev 21 | 22 | # Start production server 23 | yarn build && yarn start 24 | ``` 25 | 26 | ## The idea behind this example 27 | 28 | Replace `ream dev` and `ream start` with your own custom server, e.g. `Express`. 29 | -------------------------------------------------------------------------------- /examples/custom-server-express/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default () => ({ 7 | router: new Router({ 8 | mode: 'history', 9 | routes: [ 10 | { 11 | path: '/', 12 | component: { 13 | render: h => h('h1', 'hello world') 14 | } 15 | } 16 | ] 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /examples/custom-server-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "custom-server-express", 4 | "scripts": { 5 | "start": "NODE_ENV=production node server", 6 | "dev": "node server", 7 | "build": "ream build" 8 | }, 9 | "dependencies": { 10 | "express": "^4.0.0", 11 | "ream": "latest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom-server-express/ream.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // ... 3 | } 4 | -------------------------------------------------------------------------------- /examples/custom-server-express/server.js: -------------------------------------------------------------------------------- 1 | const server = require('express')() 2 | const ream = require('ream') 3 | 4 | const port = process.env.PORT || 3000 5 | 6 | const app = ream() 7 | 8 | // Get the request handler for http.createServer 9 | // app.getRequestHandler() returns a Promise which resolves to the request handler 10 | app.getRequestHandler().then(handler => { 11 | server.get('*', handler) 12 | 13 | server.listen(port, err => { 14 | if (err) { 15 | console.error(err) 16 | } 17 | }) 18 | }) 19 | 20 | app.on('renderer-ready', () => { 21 | console.log(`> Open http://localhost:${port}`) 22 | }) 23 | -------------------------------------------------------------------------------- /examples/custom-server-http/README.md: -------------------------------------------------------------------------------- 1 | # Custom server with `http` 2 | 3 | ## How to use 4 | 5 | ```bash 6 | git clone https://github.com/ream/ream.git 7 | ``` 8 | 9 | Install dependencies: 10 | 11 | ```bash 12 | cd examples/custom-server-http 13 | yarn 14 | ``` 15 | 16 | Run it: 17 | 18 | ```bash 19 | # Start development server 20 | yarn dev 21 | 22 | # Start production server 23 | yarn build && yarn start 24 | ``` 25 | 26 | ## The idea behind this example 27 | 28 | Replace `ream dev` and `ream start` with your own custom server, e.g. `http.createServer`. 29 | -------------------------------------------------------------------------------- /examples/custom-server-http/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default () => ({ 7 | router: new Router({ 8 | mode: 'history', 9 | routes: [ 10 | { 11 | path: '/', 12 | component: { 13 | render: h => h('h1', 'hello world') 14 | } 15 | } 16 | ] 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /examples/custom-server-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "custom-server-http", 4 | "scripts": { 5 | "start": "NODE_ENV=production node server", 6 | "dev": "node server", 7 | "build": "ream build" 8 | }, 9 | "dependencies": { 10 | "ream": "latest" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/custom-server-http/ream.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // ... 3 | } 4 | -------------------------------------------------------------------------------- /examples/custom-server-http/server.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('http') 2 | const ream = require('ream') 3 | 4 | const port = process.env.PORT || 3000 5 | 6 | const app = ream() 7 | 8 | // Get the request handler for http.createServer 9 | // app.getRequestHandler() returns a Promise which resolves to the request handler 10 | app.getRequestHandler().then(handler => { 11 | const server = createServer((req, res) => { 12 | handler(req, res) 13 | }) 14 | 15 | server.listen(port, err => { 16 | if (err) { 17 | console.error(err) 18 | } 19 | }) 20 | }) 21 | 22 | app.on('renderer-ready', () => { 23 | console.log(`> Open http://localhost:${port}`) 24 | }) 25 | -------------------------------------------------------------------------------- /examples/fs-routes/README.md: -------------------------------------------------------------------------------- 1 | # Auto generates routes 2 | 3 | ## How to use 4 | 5 | ```bash 6 | git clone https://github.com/ream/ream.git 7 | ``` 8 | 9 | Install dependencies: 10 | 11 | ```bash 12 | cd examples/fs-routes 13 | yarn 14 | ``` 15 | 16 | Run it: 17 | 18 | ```bash 19 | # Start development server 20 | yarn dev 21 | 22 | # Start production server 23 | yarn build && yarn start 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/fs-routes/index.test.js: -------------------------------------------------------------------------------- 1 | const testProject = require('../../test/lib/testProject') 2 | 3 | testProject(__dirname, async (t, c) => { 4 | for (const url of ['/', '/user', '/user/mary']) { 5 | t.matchSnapshot((await c.axios.get(url)).data, url) 6 | } 7 | for (const url of ['/bummer', '/user/mary/bummer']) { 8 | await t.rejects(c.axios.get(url)) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /examples/fs-routes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "fs-routes", 4 | "scripts": { 5 | "dev": "ream dev", 6 | "start": "ream start", 7 | "build": "ream build" 8 | }, 9 | "dependencies": { 10 | "ream": "latest" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/fs-routes/pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/fs-routes/pages/user.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/fs-routes/pages/user/[user_id].vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /examples/fs-routes/pages/user/[user_id]/friends.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/fs-routes/pages/user/[user_id]/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/fs-routes/pages/user/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /examples/fs-routes/ream.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fsRoutes: true 3 | } 4 | -------------------------------------------------------------------------------- /examples/prepopulate-vuex/README.md: -------------------------------------------------------------------------------- 1 | # Pre-populate Vuex store 2 | 3 | ## How to use 4 | 5 | ```bash 6 | git clone https://github.com/ream/ream.git 7 | ``` 8 | 9 | Install dependencies: 10 | 11 | ```bash 12 | cd examples/prepopulate-vuex 13 | yarn 14 | ``` 15 | 16 | Run it: 17 | 18 | ```bash 19 | # Start development server 20 | yarn dev 21 | 22 | # Start production server 23 | yarn build && yarn start 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/prepopulate-vuex/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Vuex from 'vuex' 4 | 5 | Vue.use(Router) 6 | Vue.use(Vuex) 7 | 8 | export default () => ({ 9 | store: new Vuex.Store({ 10 | state: { 11 | count: 0 12 | }, 13 | mutations: { 14 | INC(state) { 15 | state.count++ 16 | } 17 | }, 18 | actions: { 19 | reamServerInit({ commit }, context) { 20 | commit('INC') 21 | } 22 | } 23 | }), 24 | router: new Router({ 25 | mode: 'history', 26 | routes: [ 27 | { 28 | path: '/', 29 | component: { 30 | render(h) { 31 | return h('h1', null, ['hello', this.$store.state.count]) 32 | } 33 | } 34 | } 35 | ] 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /examples/prepopulate-vuex/index.test.js: -------------------------------------------------------------------------------- 1 | const testProject = require('../../test/lib/testProject') 2 | 3 | testProject(__dirname, async (t, c) => { 4 | t.matchSnapshot((await c.axios.get('/')).data, 'page') 5 | }) 6 | -------------------------------------------------------------------------------- /examples/prepopulate-vuex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "ream dev", 5 | "build": "ream build", 6 | "start": "ream start" 7 | }, 8 | "dependencies": { 9 | "ream": "latest", 10 | "vuex": "^3.0.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/serve-public-files/README.md: -------------------------------------------------------------------------------- 1 | # Serve static files 2 | 3 | Docs: https://ream.js.org/guide/serve-public-files.html 4 | 5 | ## Development 6 | 7 | ```bash 8 | yarn dev 9 | ``` 10 | 11 | ## Idea behind this example 12 | 13 | Serve `./public/style.css` at `/style.css`. 14 | -------------------------------------------------------------------------------- /examples/serve-public-files/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default () => ({ 7 | router: new Router({ 8 | mode: 'history', 9 | routes: [ 10 | { 11 | path: '/', 12 | component: { 13 | head: { 14 | link: [ 15 | { 16 | rel: 'stylesheet', 17 | href: '/style.css' 18 | } 19 | ] 20 | }, 21 | render(h) { 22 | return h('h1', 'homepage') 23 | } 24 | } 25 | } 26 | ] 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /examples/serve-public-files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "serve-public-files", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "ream dev", 7 | "build": "ream build", 8 | "start": "ream start" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "ream": "latest" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/serve-public-files/public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/with-apollo/createApolloClient.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { ApolloClient } from 'apollo-client' 3 | import { HttpLink } from 'apollo-link-http' 4 | import { InMemoryCache } from 'apollo-cache-inmemory' 5 | import VueApollo from 'vue-apollo' 6 | import fetch from 'isomorphic-fetch' 7 | 8 | // Install the vue plugin 9 | Vue.use(VueApollo) 10 | 11 | // Create the apollo client 12 | export default ({ globalState }) => { 13 | const httpLink = new HttpLink({ 14 | fetch, 15 | // You should use an absolute URL here 16 | uri: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn', 17 | }) 18 | 19 | const cache = new InMemoryCache() 20 | 21 | // If on the client, recover the injected state 22 | if (process.browser) { 23 | // If on the client, recover the injected state 24 | if (typeof window !== 'undefined') { 25 | const state = globalState.apollo 26 | if (state) { 27 | // If you have multiple clients, use `state.` 28 | cache.restore(state.defaultClient) 29 | } 30 | } 31 | } 32 | 33 | const apolloClient = new ApolloClient({ 34 | link: httpLink, 35 | cache, 36 | ...(process.server ? { 37 | // Set this on the server to optimize queries when SSR 38 | ssrMode: true, 39 | } : { 40 | // This will temporary disable query force-fetching 41 | ssrForceFetchDelay: 100, 42 | }), 43 | }) 44 | 45 | return apolloClient 46 | } 47 | -------------------------------------------------------------------------------- /examples/with-apollo/index.js: -------------------------------------------------------------------------------- 1 | import VueApollo from 'vue-apollo' 2 | import createApolloClient from './createApolloClient' 3 | import createRouter from './router' 4 | 5 | export default context => { 6 | const apolloProvider = new VueApollo({ 7 | defaultClient: createApolloClient(context) // an apollo-client instance 8 | }) 9 | 10 | const router = createRouter() 11 | 12 | return { 13 | router, 14 | apolloProvider 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/with-apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-apollo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "ream dev", 7 | "build": "ream build", 8 | "start": "ream start" 9 | }, 10 | "dependencies": { 11 | "apollo-cache-inmemory": "^1.2.1", 12 | "apollo-client": "^2.3.1", 13 | "apollo-link-http": "^1.5.4", 14 | "graphql": "^0.13.2", 15 | "graphql-tag": "^2.9.2", 16 | "isomorphic-fetch": "^2.2.1", 17 | "ream": "^3.0.1", 18 | "vue-apollo": "^3.0.0-beta.13" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/with-apollo/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default () => new Router({ 7 | mode: 'history', 8 | routes: [ 9 | { 10 | path: '/', 11 | component: () => import('./views/index.vue') 12 | }, 13 | { 14 | path: '/post/:id', 15 | component: () => import('./views/post.vue') 16 | } 17 | ] 18 | }) 19 | -------------------------------------------------------------------------------- /examples/with-apollo/views/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /examples/with-apollo/views/post.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 59 | -------------------------------------------------------------------------------- /examples/with-auth/README.md: -------------------------------------------------------------------------------- 1 | # With Auth 2 | 3 | ## How to use 4 | 5 | ```bash 6 | git clone https://github.com/ream/ream.git 7 | ``` 8 | 9 | Install dependencies: 10 | 11 | ```bash 12 | cd examples/with-auth 13 | yarn 14 | ``` 15 | 16 | Run it: 17 | 18 | ```bash 19 | # Start development server 20 | yarn dev 21 | 22 | # Start production server 23 | yarn build && yarn start 24 | ``` 25 | 26 | ## The idea behind this example 27 | 28 | Redirect user to a login page from a page that requires login. 29 | -------------------------------------------------------------------------------- /examples/with-auth/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import getToken from './lib/getToken' 4 | 5 | Vue.use(Router) 6 | 7 | export default () => ({ 8 | router: new Router({ 9 | mode: 'history', 10 | routes: [ 11 | { 12 | path: '/', 13 | component: () => import('./views/Home.vue') 14 | }, 15 | { 16 | path: '/secret', 17 | meta: { 18 | requireLogin: true 19 | }, 20 | component: () => import('./views/Secret.vue') 21 | }, 22 | { 23 | path: '/login', 24 | component: () => import('./views/Login.vue') 25 | } 26 | ] 27 | }), 28 | async middleware({ route, req, redirect, error }) { 29 | if (route.meta.requireLogin) { 30 | const token = getToken(req) 31 | if (!token) { 32 | // Always redirect to /login for unauthed request 33 | redirect('/login') 34 | 35 | // Or redirect for server-side request 36 | // But continue with error message for client-side request 37 | // if (process.server) { 38 | // redirect('/login') 39 | // } else { 40 | // error({ code: 403, message: 'unauthroized' }) 41 | // } 42 | } 43 | } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /examples/with-auth/lib/getToken.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie' 2 | 3 | export default req => { 4 | const { token } = cookie.parse(req ? req.headers.cookie || '' : document.cookie) 5 | 6 | return token 7 | } 8 | -------------------------------------------------------------------------------- /examples/with-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "route-guard", 4 | "scripts": { 5 | "dev": "ream dev", 6 | "start": "ream start", 7 | "build": "ream build" 8 | }, 9 | "dependencies": { 10 | "cookie": "^0.3.1", 11 | "ream": "latest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/with-auth/views/Home.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /examples/with-auth/views/Login.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | -------------------------------------------------------------------------------- /examples/with-auth/views/Secret.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /lib/babel/addInitialDataKey.js: -------------------------------------------------------------------------------- 1 | const hash = require('hash-sum') 2 | 3 | const hasDataKey = properties => { 4 | return properties.some(prop => { 5 | return prop.type === 'ObjectProperty' && prop.key.name === 'initialDataKey' 6 | }) 7 | } 8 | 9 | module.exports = ({ types: t }) => ({ 10 | name: 'add-initial-data-key', 11 | visitor: { 12 | 'ObjectProperty|ObjectMethod'( 13 | path, 14 | { 15 | file: { 16 | opts: { filename } 17 | } 18 | } 19 | ) { 20 | if ( 21 | path.node.key.name !== 'getInitialData' || 22 | path.parentPath.isObjectPattern() || 23 | hasDataKey(path.parent.properties) 24 | ) { 25 | return 26 | } 27 | 28 | path.insertAfter( 29 | t.objectProperty( 30 | t.identifier('initialDataKey'), 31 | t.stringLiteral(hash(filename)) 32 | ) 33 | ) 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /lib/babel/addInitialDataKey.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const babel = require('@babel/core') 3 | 4 | const plugin = require.resolve('./addInitialDataKey') 5 | 6 | tap.test(async t => { 7 | const { code } = babel.transform( 8 | ` 9 | const a = { 10 | getInitialData() {} 11 | } 12 | const b = { 13 | getInitialData: () => {} 14 | } 15 | `, 16 | { 17 | babelrc: false, 18 | plugins: [plugin], 19 | filename: 'foo' 20 | } 21 | ) 22 | t.equal(code.match(/initialDataKey:/g).length, 2) 23 | }) 24 | 25 | tap.test('ignore object decomposition', async t => { 26 | const { code } = babel.transform( 27 | ` 28 | const { getInitialData } = Component 29 | `, 30 | { 31 | babelrc: false, 32 | plugins: [plugin], 33 | filename: 'foo' 34 | } 35 | ) 36 | t.equal(code.match(/initialDataKey:/g), null) 37 | }) 38 | -------------------------------------------------------------------------------- /lib/babel/preset.js: -------------------------------------------------------------------------------- 1 | const env = process.env.BABEL_ENV || process.env.NODE_ENV 2 | 3 | module.exports = (ctx, { isServer, defaultBabelPreset }) => { 4 | if (defaultBabelPreset === false) { 5 | return {} 6 | } 7 | 8 | const presets = [ 9 | [ 10 | require.resolve('@babel/preset-env'), 11 | { 12 | modules: env === 'test', 13 | targets: 14 | isServer || env === 'test' 15 | ? { 16 | node: 'current' 17 | } 18 | : { 19 | ie: 9 20 | } 21 | } 22 | ] 23 | ] 24 | 25 | let plugins = [require.resolve('./addInitialDataKey')] 26 | 27 | if (defaultBabelPreset === 'minimal') { 28 | return { 29 | presets, 30 | plugins 31 | } 32 | } 33 | 34 | plugins = [ 35 | ...plugins, 36 | require.resolve('@babel/plugin-proposal-class-properties'), 37 | [ 38 | require.resolve('@babel/plugin-transform-runtime'), 39 | { 40 | helpers: false, 41 | regenerator: true 42 | } 43 | ], 44 | [ 45 | require.resolve('@babel/plugin-proposal-object-rest-spread'), 46 | { 47 | useBuiltIns: true 48 | } 49 | ], 50 | // For dynamic import that you will use a lot in code-split 51 | require.resolve('@babel/plugin-syntax-dynamic-import'), 52 | require.resolve('babel-plugin-transform-vue-jsx'), 53 | [ 54 | require.resolve('babel-plugin-webpack-chunkname'), 55 | { 56 | getChunkName(name) { 57 | return ( 58 | 'chunk-' + 59 | name 60 | .replace(/\.[a-z0-9]{2,5}$/, '') 61 | .replace(/[^a-z0-9]+/gi, '-') 62 | .toLowerCase() 63 | ) 64 | } 65 | } 66 | ] 67 | ] 68 | 69 | return { 70 | presets, 71 | plugins 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/emoji.js: -------------------------------------------------------------------------------- 1 | const supportsEmoji = 2 | process.platform !== 'win32' || process.env.TERM === 'xterm-256color' 3 | 4 | // Fallback symbols for Windows from https://en.wikipedia.org/wiki/Code_page_437 5 | module.exports = { 6 | progress: supportsEmoji ? '⏳' : '∞', 7 | success: supportsEmoji ? '✨' : '√', 8 | error: supportsEmoji ? '🚨' : '×', 9 | warning: supportsEmoji ? '⚠️' : '‼' 10 | } 11 | -------------------------------------------------------------------------------- /lib/hooks.js: -------------------------------------------------------------------------------- 1 | class Hooks { 2 | constructor() { 3 | this.hooks = new Map() 4 | } 5 | 6 | add(name, handler) { 7 | if (!handler) return 8 | if (!this.hooks.has(name)) { 9 | this.hooks.set(name, new Set()) 10 | } 11 | const hooks = this.hooks.get(name) 12 | hooks.add(handler) 13 | return this 14 | } 15 | 16 | async run(name, context) { 17 | if (!this.hooks.has(name)) return 18 | for (const hook of this.hooks.get(name)) { 19 | // eslint-disable-next-line no-await-in-loop 20 | await hook(context) 21 | } 22 | } 23 | } 24 | 25 | module.exports = new Hooks() 26 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import webpack from 'webpack' 3 | import WebpackChainConfig from 'webpack-chain' 4 | 5 | export interface LogOptions { 6 | logLevel: number 7 | debug: boolean 8 | silly: boolean 9 | quiet: boolean 10 | logUpdate: boolean 11 | } 12 | 13 | export interface Options extends LogOptions { 14 | dev: boolean 15 | baseDir: string 16 | config: string | false 17 | inspectWebpack: boolean 18 | } 19 | 20 | export interface WebpackConfigContext { 21 | isServer: boolean 22 | isClient: boolean 23 | dev: boolean 24 | type: string 25 | } 26 | 27 | export type ChainWebpackFn = (config: WebpackChainConfig, context: WebpackConfigContext) => void 28 | 29 | export type ConfigureWebpackFn = (config: webpack.Configuration, context: WebpackConfigContext) => void 30 | 31 | export type ConfigureServerFn = (server: express.Express) => void 32 | 33 | export type GeneratedRoutes = string[] 34 | 35 | export interface PluginDef { 36 | name: string 37 | apply: (ream: Ream) => void 38 | } 39 | 40 | export interface GenerateOptions { 41 | routes: GeneratedRoutes 42 | } 43 | 44 | export interface Config { 45 | entry: string 46 | outDir: string 47 | fsRoutes: boolean | { 48 | baseDir: string 49 | basePath: string 50 | match: RegExp 51 | } 52 | transpileDependencies: string[] 53 | runtimeCompiler: boolean 54 | productionSourceMap: boolean 55 | chainWebpack: ChainWebpackFn 56 | configureWebpack: ConfigureWebpackFn 57 | server: { 58 | port: number 59 | host: string 60 | } 61 | plugins: PluginDef[] 62 | generate: GenerateOptions 63 | css: { 64 | extract: boolean 65 | } 66 | pwa: boolean 67 | minimize: boolean 68 | defaultBabelPreset: 'minimal' | false 69 | } 70 | 71 | export class Ream { 72 | constructor(options?: Partial, config?: Partial) 73 | chainWebpack(ChainWebpackFn): void 74 | addGenerateRoutes(GeneratedRoutes): void 75 | hasPlugin(string): PluginDef 76 | loadPlugins(): void 77 | createConfigs(): void 78 | createCompilers(): void 79 | build(): Promise 80 | generate(opts?: GenerateOptions): Promise 81 | generateOnly(opts?: GenerateOptions): Promise 82 | configureServer(fn: ConfigureServerFn): void 83 | prepareFiles(): Promise 84 | writeEnhanceAppFile(): Promise 85 | writeEntryFile(): Promise 86 | getServer(): Promise 87 | getRequestHandler(): Promise 88 | start(): Promise 89 | prepareWebpack(): Promise 90 | prepareProduction(): Promise 91 | createRenderer({ serverBundle, clientManifest, serverType }: { serverBundle: string | object, clientManifest: object, serverType: 'generate' | 'production' }): void 92 | resolveOutDir(...args): string 93 | resolveBaseDir(...args): string 94 | } 95 | 96 | export default function ream(options?: Partial, config?: Partial): Ream 97 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const Event = require('events') 3 | const fs = require('fs-extra') 4 | const Config = require('webpack-chain') 5 | const express = require('express') 6 | const chalk = require('chalk') 7 | const chokidar = require('chokidar') 8 | const merge = require('lodash.merge') 9 | const { createBundleRenderer } = require('vue-server-renderer') 10 | const loadConfig = require('./utils/loadConfig') 11 | const basePlugin = require('./plugins/base') 12 | const logger = require('./logger') 13 | const serveStatic = require('./utils/serveStatic') 14 | const renderHtml = require('./utils/renderHtml') 15 | const minifyHtml = require('./utils/minifyHtml') 16 | const emoji = require('./emoji') 17 | const inspect = require('./utils/inspect') 18 | const validateConfig = require('./validateConfig') 19 | 20 | const runWebpack = compiler => { 21 | return new Promise((resolve, reject) => { 22 | compiler.run((err, stats) => { 23 | if (err) return reject(err) 24 | resolve(stats) 25 | }) 26 | }) 27 | } 28 | 29 | class Ream extends Event { 30 | constructor(options, config) { 31 | super() 32 | this.options = merge( 33 | { 34 | dev: process.env.NODE_ENV !== 'production', 35 | baseDir: '.', 36 | config: 'ream.config.js' 37 | }, 38 | options 39 | ) 40 | 41 | // Init logger options 42 | logger.setOptions(options) 43 | 44 | // Load ream config 45 | let userConfig 46 | if (this.options.config !== false) { 47 | const loadedConfig = loadConfig.loadSync( 48 | [this.options.config], 49 | this.options.baseDir 50 | ) 51 | if (loadedConfig.path) { 52 | logger.debug('ream config', loadedConfig.path) 53 | userConfig = loadedConfig.data 54 | } 55 | } 56 | const validatedUserConfig = validateConfig(userConfig || {}) 57 | if (validatedUserConfig.error) { 58 | throw validatedUserConfig.error 59 | } 60 | 61 | const defaults = { 62 | entry: 'index.js', 63 | outDir: '.ream', 64 | fsRoutes: false, 65 | transpileDependencies: [], 66 | runtimeCompiler: false, 67 | productionSourceMap: true, 68 | server: { 69 | port: process.env.PORT || 4000, 70 | host: process.env.HOST || '0.0.0.0' 71 | }, 72 | plugins: [], 73 | generate: { 74 | routes: ['/'] 75 | }, 76 | css: { 77 | extract: !this.options.dev 78 | }, 79 | pwa: false, 80 | minimize: !this.options.dev, 81 | minifyHtml: false 82 | } 83 | 84 | // config from constructor can override user config 85 | this.config = merge(defaults, validatedUserConfig.value, config) 86 | 87 | logger.debug('ream config', inspect(this.config)) 88 | 89 | this.hooks = require('./hooks') 90 | this.configureServerFns = new Set() 91 | this.enhanceAppFiles = new Set() 92 | this.serverConfig = new Config() 93 | this.clientConfig = new Config() 94 | this.chainWebpackFns = [] 95 | 96 | const projectPkg = loadConfig.loadSync(['package.json']) 97 | this.projectPkg = { 98 | data: projectPkg.data || {}, 99 | path: projectPkg.path 100 | } 101 | 102 | this.loadPlugins() 103 | } 104 | 105 | chainWebpack(fn) { 106 | this.chainWebpackFns.push(fn) 107 | } 108 | 109 | addGenerateRoutes(routes) { 110 | this.config.generate.routes = this.config.generate.routes.concat(routes) 111 | return this 112 | } 113 | 114 | hasPlugin(name) { 115 | return this.plugins && this.plugins.find(plugin => plugin.name === name) 116 | } 117 | 118 | loadPlugins() { 119 | this.plugins = [ 120 | basePlugin, 121 | require('./plugins/vuex'), 122 | require('./plugins/apollo'), 123 | require('./plugins/pwa'), 124 | require('./plugins/fs-routes') 125 | ] 126 | .concat(this.config.plugins) 127 | .filter(Boolean) 128 | 129 | this.plugins.forEach(plugin => plugin.apply(this)) 130 | } 131 | 132 | createConfigs() { 133 | const getContext = type => ({ 134 | isServer: type === 'server', 135 | isClient: type === 'client', 136 | dev: this.options.dev, 137 | type 138 | }) 139 | 140 | if (this.config.chainWebpack) { 141 | this.chainWebpack(this.config.chainWebpack) 142 | } 143 | for (const fn of this.chainWebpackFns) { 144 | fn(this.serverConfig, getContext('server')) 145 | fn(this.clientConfig, getContext('client')) 146 | } 147 | 148 | if (this.options.inspectWebpack) { 149 | console.log('server config', chalk.dim(this.serverConfig.toString())) 150 | console.log('client config', chalk.dim(this.clientConfig.toString())) 151 | } 152 | 153 | let serverConfig = this.serverConfig.toConfig() 154 | let clientConfig = this.clientConfig.toConfig() 155 | 156 | const { configureWebpack } = this.config 157 | if (typeof configureWebpack === 'function') { 158 | serverConfig = 159 | configureWebpack(serverConfig, getContext('server')) || serverConfig 160 | clientConfig = 161 | configureWebpack(clientConfig, getContext('client')) || clientConfig 162 | } 163 | 164 | return [serverConfig, clientConfig] 165 | } 166 | 167 | createCompilers() { 168 | const webpack = require('webpack') 169 | 170 | return this.createConfigs().map(config => webpack(config)) 171 | } 172 | 173 | async build() { 174 | await fs.remove(this.resolveOutDir()) 175 | await this.prepareWebpack() 176 | const [serverCompiler, clientCompiler] = this.createCompilers() 177 | await Promise.all([runWebpack(serverCompiler), runWebpack(clientCompiler)]) 178 | if (!this.isGenerating) { 179 | await this.hooks.run('onFinished', 'build') 180 | } 181 | } 182 | 183 | async generate(opts) { 184 | this.isGenerating = true 185 | await this.build() 186 | await this.generateOnly(opts) 187 | await this.hooks.run('onFinished', 'generate') 188 | } 189 | 190 | async generateOnly({ routes } = this.config.generate) { 191 | routes = [...new Set(routes)] 192 | // Not an actually server 193 | // Don't emit `server-ready` event 194 | this.prepareProduction({ serverType: 'generate' }) 195 | 196 | // Copy public folder to root path of `generated` 197 | if (await fs.pathExists('./public')) { 198 | await fs.copy('./public', this.resolveOutDir('generated')) 199 | } 200 | 201 | // Copy webpack assets to `generated` 202 | await fs.copy( 203 | this.resolveOutDir('client'), 204 | this.resolveOutDir('generated/_ream') 205 | ) 206 | 207 | // Remove unnecessary files 208 | await fs.remove(this.resolveOutDir('generated/_ream/client-manifest.json')) 209 | 210 | await Promise.all( 211 | routes.map(async route => { 212 | // Fake req 213 | const context = { req: { url: route } } 214 | const html = await this.renderHtml(context) 215 | const targetPath = this.resolveOutDir( 216 | `generated/${route.replace(/\/?$/, '/index.html')}` 217 | ) 218 | 219 | logger.status(emoji.progress, `generating ${route}`) 220 | 221 | await fs.ensureDir(path.dirname(targetPath)) 222 | await fs.writeFile(targetPath, html, 'utf8') 223 | }) 224 | ) 225 | 226 | logger.status( 227 | emoji.success, 228 | chalk.green( 229 | `Check out ${path.relative( 230 | process.cwd(), 231 | this.resolveOutDir('generated') 232 | )}` 233 | ) 234 | ) 235 | } 236 | 237 | configureServer(fn) { 238 | this.configureServerFns.add(fn) 239 | } 240 | 241 | async prepareFiles() { 242 | await fs.ensureDir(this.resolveOutDir()) 243 | await Promise.all([ 244 | this.writeEnhanceAppFile(), 245 | this.writeEntryFile(), 246 | this.hooks.run('onPrepareFiles') 247 | ]) 248 | } 249 | 250 | writeEnhanceAppFile() { 251 | return fs.writeFile( 252 | this.resolveOutDir('enhance-app.js'), 253 | require('../app/enhance-app-template')(this) 254 | ) 255 | } 256 | 257 | async writeEntryFile() { 258 | const writeFile = () => 259 | fs.writeFile( 260 | this.resolveOutDir('entry.js'), 261 | require('../app/entry-template')(this) 262 | ) 263 | writeFile() 264 | if (this.config.entry && this.options.dev) { 265 | chokidar 266 | .watch(this.resolveBaseDir(this.config.entry), { 267 | disableGlobbing: true, 268 | ignoreInitial: true 269 | }) 270 | .on('add', writeFile) 271 | .on('unlink', writeFile) 272 | } 273 | } 274 | 275 | async getServer() { 276 | const server = express() 277 | 278 | for (const fn of this.configureServerFns) { 279 | fn(server) 280 | } 281 | 282 | // Compress 283 | server.use(require('compression')()) 284 | 285 | // Serve ./public folder at root path 286 | server.use(serveStatic('public', !this.options.dev)) 287 | 288 | if (this.options.dev) { 289 | await this.prepareWebpack() 290 | require('./utils/setupWebpackMiddlewares')(this)(server) 291 | } else { 292 | this.prepareProduction({ serverType: 'production' }) 293 | server.get('/_ream/*', (req, res, ...args) => { 294 | req.url = req.url.replace(/^\/_ream/, '') 295 | serveStatic( 296 | this.resolveOutDir('client'), 297 | typeof req.shouldCache === 'boolean' ? req.shouldCache : true // Cache 298 | )(req, res, ...args) 299 | }) 300 | } 301 | 302 | const handleError = fn => { 303 | return async (req, res) => { 304 | try { 305 | await fn(req, res) 306 | } catch (err) { 307 | if (err.name === 'ReamError') { 308 | if (err.code === 'REDIRECT') { 309 | res.writeHead(303, { Location: err.redirectURL }) 310 | res.end() 311 | return 312 | } 313 | } 314 | 315 | res.status(500) 316 | if (this.options.dev) { 317 | res.end(err.stack) 318 | } else { 319 | res.end('server error') 320 | } 321 | 322 | console.log(err.stack) 323 | } 324 | } 325 | } 326 | 327 | server.get( 328 | '*', 329 | handleError(async (req, res) => { 330 | if (!this.renderer) { 331 | return res.end('Please wait for compilation...') 332 | } 333 | 334 | if (req.url.startsWith('/_ream/')) { 335 | res.status(404) 336 | return res.end('404') 337 | } 338 | 339 | const context = { req, res } 340 | const html = await this.renderHtml(context) 341 | 342 | res.setHeader('content-type', 'text/html') 343 | res.end(html) 344 | }) 345 | ) 346 | 347 | return server 348 | } 349 | 350 | async getRequestHandler() { 351 | const server = await this.getServer() 352 | return (req, res) => server(req, res) 353 | } 354 | 355 | async start() { 356 | const server = await this.getServer() 357 | return server.listen(this.config.server.port, this.config.server.host) 358 | } 359 | 360 | async prepareWebpack() { 361 | let postcssConfigFile 362 | if (this.projectPkg.path && this.projectPkg.data.postcss) { 363 | postcssConfigFile = this.projectPkg.path 364 | } else { 365 | const res = loadConfig.loadSync([ 366 | 'postcss.config.js', 367 | '.postcssrc', 368 | '.postcssrc.js' 369 | ]) 370 | postcssConfigFile = res.path 371 | } 372 | 373 | if (postcssConfigFile) { 374 | logger.debug('postcss config file', postcssConfigFile) 375 | this.config.postcss = { 376 | config: { 377 | path: postcssConfigFile 378 | } 379 | } 380 | } 381 | 382 | await this.prepareFiles() 383 | } 384 | 385 | prepareProduction({ serverType } = {}) { 386 | const serverBundle = JSON.parse( 387 | fs.readFileSync(this.resolveOutDir('server/server-bundle.json'), 'utf8') 388 | ) 389 | const clientManifest = JSON.parse( 390 | fs.readFileSync(this.resolveOutDir('client/client-manifest.json'), 'utf8') 391 | ) 392 | 393 | this.createRenderer({ 394 | serverBundle, 395 | clientManifest, 396 | serverType 397 | }) 398 | } 399 | 400 | createRenderer({ serverBundle, clientManifest, serverType }) { 401 | this.renderer = createBundleRenderer(serverBundle, { 402 | runInNewContext: false, 403 | clientManifest 404 | }) 405 | this.emit('renderer-ready', serverType) 406 | } 407 | 408 | async renderHtml(context) { 409 | let html = await renderHtml(this.renderer, context) 410 | if (this.config.minifyHtml) { 411 | html = minifyHtml(html, this.config.minifyHtml) 412 | } 413 | return html 414 | } 415 | 416 | resolveOutDir(...args) { 417 | return this.resolveBaseDir(this.config.outDir, ...args) 418 | } 419 | 420 | resolveBaseDir(...args) { 421 | return path.resolve(this.options.baseDir, ...args) 422 | } 423 | } 424 | 425 | function ream(opts, config) { 426 | return new Ream(opts, config) 427 | } 428 | 429 | module.exports = ream 430 | module.exports.Ream = Ream 431 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const logUpdate = require('log-update') 2 | const chalk = require('chalk') 3 | const emoji = require('./emoji') 4 | 5 | class Logger { 6 | constructor(options) { 7 | if (options) { 8 | this.setOptions(options) 9 | } 10 | } 11 | 12 | setOptions({ logLevel, debug, silly, quiet, logUpdate } = {}) { 13 | if (debug) { 14 | logLevel = 4 15 | } else if (quiet) { 16 | logLevel = 1 17 | } else if (silly) { 18 | logLevel = 5 19 | } 20 | this.logLevel = typeof logLevel === 'number' ? logLevel : 3 21 | this.useLogUpdate = typeof logUpdate === 'boolean' ? logUpdate : true 22 | } 23 | 24 | clear() { 25 | if (this.useLogUpdate) { 26 | logUpdate.clear() 27 | } 28 | } 29 | 30 | write(message, persistent = false) { 31 | if (persistent) { 32 | this.clear() 33 | console.log(message) 34 | return 35 | } 36 | if (this.useLogUpdate) { 37 | logUpdate(message) 38 | } else { 39 | console.log(message) 40 | } 41 | } 42 | 43 | // Debug message 44 | // Always persisted 45 | debug(title, message = '') { 46 | if (this.logLevel < 4) { 47 | return 48 | } 49 | 50 | this.write(`${chalk.bold(title)} ${chalk.dim(message)}`, true) 51 | } 52 | 53 | // Like debug for even more debug... 54 | silly(title, message) { 55 | if (this.logLevel < 5) { 56 | return 57 | } 58 | 59 | this.write(`${chalk.bold(title)} ${chalk.dim(message)}`, true) 60 | } 61 | 62 | // Normal log 63 | // Persist by default 64 | log(message, update) { 65 | if (this.logLevel < 3) { 66 | return 67 | } 68 | 69 | this.write(message, !update) 70 | } 71 | 72 | // Warn status 73 | warn(message) { 74 | if (this.logLevel < 2) { 75 | return 76 | } 77 | 78 | this.status(emoji.warning, message) 79 | } 80 | 81 | // Error status 82 | error(err) { 83 | if (this.logLevel < 1) { 84 | return 85 | } 86 | 87 | // TODO: handle error class too 88 | return this.status(emoji.error, err) 89 | } 90 | 91 | // Status message should be persisted 92 | // Unless `update` is set 93 | status(emoji, message, update) { 94 | if (this.logLevel < 3) { 95 | return 96 | } 97 | 98 | if (update && this.useLogUpdate) { 99 | return logUpdate(`${emoji} ${message}`) 100 | } 101 | 102 | this.clear() 103 | console.log(`${emoji} ${message}`) 104 | } 105 | } 106 | 107 | module.exports = new Logger() 108 | -------------------------------------------------------------------------------- /lib/plugins/apollo/apollo-inject.js: -------------------------------------------------------------------------------- 1 | export default ctx => { 2 | const { apolloProvider } = ctx.entry 3 | if (!apolloProvider) return 4 | 5 | ctx.rootOptions.provide = apolloProvider.provide() 6 | 7 | ctx.addMiddleware(async mContext => { 8 | if (process.browser) return 9 | 10 | await apolloProvider.prefetchAll( 11 | mContext, 12 | mContext.router.getMatchedComponents() 13 | ) 14 | ctx.globalState.apollo = apolloProvider.getStates() 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /lib/plugins/apollo/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | name: 'builtin:apollo', 5 | apply(api) { 6 | api.enhanceAppFiles.add(path.join(__dirname, 'apollo-inject.js')) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/plugins/base.js: -------------------------------------------------------------------------------- 1 | const serverConfig = require('../webpack/webpack.config.server') 2 | const clientConfig = require('../webpack/webpack.config.client') 3 | 4 | module.exports = { 5 | name: 'builtin:base', 6 | apply(api) { 7 | api.chainWebpack((config, { isServer }) => { 8 | if (isServer) { 9 | serverConfig(api, config) 10 | } else { 11 | clientConfig(api, config) 12 | } 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/empty/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/empty/.keep -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/ignore-garbage/garbage.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/ignore-garbage/garbage.txt -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/ignore-garbage/page.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/ignore-garbage/page.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/nested-children/foo.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/nested-children/foo.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/nested-children/foo/bar.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/nested-children/foo/bar.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/nested-children/foo/bar/baz.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/nested-children/foo/bar/baz.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/nested-children/foo/bar/baz/qux.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/nested-children/foo/bar/baz/qux.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/nested/foo/bar/baz/qux.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/typical/foo.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/typical/foo.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/typical/index.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/typical/index.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/typical/user.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/typical/user.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/typical/user/[user].vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/typical/user/[user].vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/typical/user/[user]/friends.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/typical/user/[user]/friends.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/presets/typical/user/[user]/index.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/presets/typical/user/[user]/index.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/routes.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const tap = require('tap') 3 | const fs = require('fs-extra') 4 | const { collectRoutes } = require('../routes-template') 5 | 6 | function testCollectRoutes(dir, options) { 7 | return tap.test(dir, async t => { 8 | const pagesDir = path.resolve(__dirname, dir) 9 | const routes = await collectRoutes( 10 | { 11 | pagesDir, 12 | componentPrefix: '#base', 13 | basePath: '/', 14 | ...options 15 | }, 16 | { 17 | match: /\.vue$/, 18 | ...(options && options.options) 19 | } 20 | ) 21 | t.matchSnapshot(routes, 'routes') 22 | }) 23 | } 24 | 25 | for (const preset of fs.readdirSync(path.resolve(__dirname, 'presets'))) { 26 | testCollectRoutes(path.join('presets', preset)) 27 | } 28 | 29 | testCollectRoutes('special/typescript', { 30 | options: { 31 | match: /\.(vue|ts)$/ 32 | } 33 | }) 34 | 35 | testCollectRoutes('special/custom-base-path', { 36 | basePath: '/some' 37 | }) 38 | 39 | tap.test('Return empty routes for non-existent directory', async () => { 40 | tap.same( 41 | await collectRoutes({ 42 | dir: path.resolve(__dirname, 'no-such-path') 43 | }), 44 | [] 45 | ) 46 | }) 47 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/special/custom-base-path/_expected_routes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "/some/foo", 4 | "component": "#base/foo.vue" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/special/custom-base-path/foo.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/special/custom-base-path/foo.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/special/typescript/_expected_routes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "/", 4 | "component": "#base/index.vue" 5 | }, 6 | { 7 | "path": "/page", 8 | "component": "#base/page.ts" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/special/typescript/index.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/special/typescript/index.vue -------------------------------------------------------------------------------- /lib/plugins/fs-routes/__test__/special/typescript/page.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ream/deprecated/326a3f865ba070515aa9968b524faa37f6aba06b/lib/plugins/fs-routes/__test__/special/typescript/page.ts -------------------------------------------------------------------------------- /lib/plugins/fs-routes/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const writeRoutes = require('./write-routes') 3 | 4 | module.exports = { 5 | name: 'builtin:fs-routes', 6 | apply(api) { 7 | if (api.config.fsRoutes) { 8 | api.enhanceAppFiles.add(path.join(__dirname, 'inject.js')) 9 | const fsRoutes = Object.assign( 10 | { 11 | baseDir: 'pages', 12 | basePath: '/', 13 | match: /\.(vue|js)$/i 14 | }, 15 | api.config.fsRoutes 16 | ) 17 | writeRoutes(api, 'routes.js', fsRoutes) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/inject.js: -------------------------------------------------------------------------------- 1 | import routes from '#out/routes' // eslint-disable-line import/no-unresolved 2 | 3 | export default ({ rootOptions }) => { 4 | rootOptions.router.addRoutes(routes) 5 | } 6 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/routes-template.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const sortBy = require('lodash.sortby') 4 | 5 | module.exports = async (collectOptions, routeOptions) => { 6 | const routes = await collectRoutes(collectOptions, routeOptions) 7 | return `export default ${renderRoutes(routes)}` 8 | } 9 | 10 | function renderRoutes(routes) { 11 | return ` 12 | [ 13 | ${routes 14 | .map( 15 | route => ` 16 | { 17 | path: ${JSON.stringify(route.path)}, 18 | component: () => import(${JSON.stringify(route.component)}), 19 | ${route.children ? `children: ${renderRoutes(route.children)}` : ``} 20 | }` 21 | ) 22 | .join(',')} 23 | ]` 24 | } 25 | 26 | // index.vue -> / 27 | // about.vue -> /about 28 | // user.vue -> /user 29 | // user/index.vue -> /user, child '' 30 | // user/friends.vue -> /user, child 'friends' 31 | // catalog/index.vue -> /catalog 32 | // catalog/specials.vue -> /catalog/specials 33 | // [path].vue -> /:path 34 | 35 | class FileCollector { 36 | constructor() { 37 | this.records = [] 38 | this.lookup = {} 39 | } 40 | 41 | add(path, props) { 42 | if (!this.lookup[path]) { 43 | this.lookup[path] = { 44 | path: filePathToRoutePath(path) 45 | } 46 | this.records.push(this.lookup[path]) 47 | } 48 | Object.assign(this.lookup[path], props) 49 | } 50 | 51 | sortedRecords() { 52 | return sortBy(this.records, record => [ 53 | record.path.indexOf(':') >= 0, // Dynamic routes go last 54 | record.path 55 | ]) 56 | } 57 | } 58 | 59 | function filePathToRoutePath(path) { 60 | if (path.toLowerCase() === 'index') { 61 | return '' 62 | } 63 | return path.replace(/\[(.*?)\]/g, ':$1') 64 | } 65 | 66 | async function collectRoutes({ pagesDir, componentPrefix, basePath }, options) { 67 | if (!(await fs.pathExists(pagesDir))) { 68 | return [] 69 | } 70 | 71 | const collector = new FileCollector() 72 | 73 | for (const name of await fs.readdir(pagesDir)) { 74 | if (name.match(/^[._]/)) { 75 | continue 76 | } 77 | const stats = await fs.stat(path.join(pagesDir, name)) // eslint-disable-line no-await-in-loop 78 | if (stats.isDirectory()) { 79 | collector.add(name, { dir: name }) 80 | } else if (stats.isFile()) { 81 | if (name.match(options.match)) { 82 | collector.add(path.basename(name, path.extname(name)), { file: name }) 83 | } 84 | } 85 | } 86 | 87 | const routes = [] 88 | 89 | for (const record of collector.sortedRecords()) { 90 | const routePath = basePath ? path.join(basePath, record.path) : record.path 91 | if (record.file) { 92 | const route = { 93 | path: routePath, 94 | component: path.join(componentPrefix, record.file) 95 | } 96 | if (record.dir) { 97 | // eslint-disable-next-line no-await-in-loop 98 | route.children = await collectRoutes( 99 | { 100 | pagesDir: path.join(pagesDir, record.dir), 101 | componentPrefix: path.join(componentPrefix, record.dir), 102 | basePath: '' 103 | }, 104 | options 105 | ) 106 | } 107 | routes.push(route) 108 | } else if (record.dir) { 109 | routes.push( 110 | // eslint-disable-next-line no-await-in-loop 111 | ...(await collectRoutes( 112 | { 113 | pagesDir: path.join(pagesDir, record.dir), 114 | componentPrefix: path.join(componentPrefix, record.dir), 115 | basePath: routePath 116 | }, 117 | options 118 | )) 119 | ) 120 | } 121 | } 122 | 123 | return routes 124 | } 125 | 126 | module.exports.collectRoutes = collectRoutes 127 | -------------------------------------------------------------------------------- /lib/plugins/fs-routes/write-routes.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const chokidar = require('chokidar') 4 | const debounce = require('lodash.debounce') 5 | const Mutex = require('promise-mutex') 6 | 7 | module.exports = async function(api, moduleName, options) { 8 | const { baseDir, basePath, ...routeOptions } = options 9 | const pagesDir = api.resolveBaseDir(baseDir) 10 | const componentPrefix = path.join('#base', baseDir) 11 | const moduleFile = api.resolveOutDir(moduleName) 12 | 13 | const collectOptions = { 14 | pagesDir, 15 | componentPrefix, 16 | basePath 17 | } 18 | 19 | const writeFile = async () => { 20 | return fs.writeFile( 21 | moduleFile, 22 | await require('./routes-template')(collectOptions, routeOptions) 23 | ) 24 | } 25 | 26 | api.hooks.add('onPrepareFiles', async () => { 27 | await writeFile() 28 | if (api.options.dev) { 29 | const mutex = new Mutex() 30 | chokidar 31 | .watch(pagesDir, { 32 | disableGlobbing: true, 33 | ignoreInitial: true 34 | }) 35 | .on( 36 | 'all', 37 | debounce(() => { 38 | mutex.lock(writeFile) 39 | }, 500) 40 | ) 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /lib/plugins/pwa/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | name: 'builtin:pwa', 5 | apply(api) { 6 | const pwa = Boolean(api.config.pwa) 7 | 8 | api.chainWebpack(config => { 9 | config.plugin('constants').tap(([options]) => [ 10 | Object.assign(options, { 11 | __PWA_ENABLED__: JSON.stringify(pwa) 12 | }) 13 | ]) 14 | }) 15 | 16 | if (pwa) { 17 | api.enhanceAppFiles.add(path.join(__dirname, 'pwa-inject.js')) 18 | 19 | api.hooks.add('onFinished', async type => { 20 | const { generateSW } = require('workbox-build') 21 | await generateSW({ 22 | swDest: api.resolveOutDir( 23 | type === 'build' ? 'client/sw.js' : 'generated/_ream/sw.js' 24 | ), 25 | globDirectory: 26 | type === 'build' 27 | ? api.resolveOutDir('client') 28 | : api.resolveOutDir('generated'), 29 | globPatterns: [ 30 | '**/*.{js,css,html,png,jpg,jpeg,gif,svg,woff,woff2,eot,ttf,otf}' 31 | ], 32 | modifyUrlPrefix: { 33 | '': type === 'build' ? '/_ream/' : '/' 34 | } 35 | }) 36 | }) 37 | } 38 | 39 | api.configureServer(app => { 40 | if (api.options.dev) { 41 | app.use(require('./noop-sw-middleware')()) 42 | } else { 43 | app.use('/_ream/sw.js', (req, res, next) => { 44 | req.shouldCache = false 45 | next() 46 | }) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/plugins/pwa/noop-sw-middleware.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const resetScript = fs.readFileSync(path.join(__dirname, 'noop-sw.js'), 'utf-8') 5 | 6 | module.exports = function() { 7 | return function(req, res, next) { 8 | if (req.url === '/sw.js') { 9 | res.setHeader('Content-Type', 'text/javascript') 10 | res.send(resetScript) 11 | } else { 12 | next() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/plugins/pwa/noop-sw.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | self.addEventListener('install', function(e) { 3 | self.skipWaiting() 4 | }) 5 | 6 | self.addEventListener('activate', function(e) { 7 | self.registration 8 | .unregister() 9 | .then(function() { 10 | return self.clients.matchAll() 11 | }) 12 | .then(function(clients) { 13 | clients.forEach(client => client.navigate(client.url)) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /lib/plugins/pwa/pwa-inject.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | if ( 3 | process.browser && 4 | !__DEV__ && 5 | __PWA_ENABLED__ && 6 | window.location.protocol === 'https:' 7 | ) { 8 | const { register } = require('register-service-worker') 9 | 10 | register(`${__PUBLIC_PATH__}sw.js`, { 11 | ready() { 12 | console.log('[ream:pwa] Service worker is active.') 13 | }, 14 | cached() { 15 | console.log('[ream:pwa] Content has been cached for offline use.') 16 | }, 17 | updated() { 18 | console.log('[ream:pwa] Content updated.') 19 | }, 20 | offline() { 21 | console.log( 22 | '[ream:pwa] No internet connection found. App is running in offline mode.' 23 | ) 24 | }, 25 | error(err) { 26 | console.error('[ream:pwa] Error during service worker registration:', err) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /lib/plugins/vuex/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | name: 'builtin:vuex', 5 | apply(api) { 6 | api.enhanceAppFiles.add(path.join(__dirname, 'inject.js')) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/plugins/vuex/inject.js: -------------------------------------------------------------------------------- 1 | export default ({ globalState, entry, rootOptions, event, addMiddleware }) => { 2 | const { store } = entry 3 | 4 | if (!store) return 5 | 6 | rootOptions.store = store 7 | 8 | addMiddleware(context => { 9 | context.store = store 10 | if (process.server && store._actions.reamServerInit) { 11 | return store.dispatch('reamServerInit', context) 12 | } 13 | }) 14 | 15 | event.$on('before-server-render', () => { 16 | globalState.state = store.state 17 | }) 18 | 19 | event.$on('before-client-render', () => { 20 | store.replaceState(globalState.state) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /lib/utils/dir.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.ownDir = (...args) => { 4 | return path.join(__dirname, '../../', ...args) 5 | } 6 | 7 | exports.inWorkspace = __dirname.indexOf('/packages/ream/lib/') > 0 8 | -------------------------------------------------------------------------------- /lib/utils/document.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ headTags, scripts }) => 2 | ` 3 | 4 | 5 | 6 | 7 | 8 | ${headTags()} 9 | 10 | 11 | 12 | ${scripts()} 13 | 14 | 15 | `.trim() 16 | -------------------------------------------------------------------------------- /lib/utils/inspect.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | 3 | module.exports = obj => 4 | util.inspect(obj, { 5 | depth: null, 6 | colors: true 7 | }) 8 | -------------------------------------------------------------------------------- /lib/utils/loadConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const JoyCon = require('joycon').default 3 | 4 | module.exports = new JoyCon({ 5 | stopDir: path.dirname(process.cwd()) 6 | }) 7 | -------------------------------------------------------------------------------- /lib/utils/minifyHtml.js: -------------------------------------------------------------------------------- 1 | const { minify } = require('html-minifier') 2 | 3 | const defaultOptions = { 4 | collapseWhitespace: true, 5 | removeRedundantAttributes: true, 6 | removeScriptTypeAttributes: true, 7 | removeStyleLinkTypeAttributes: true, 8 | useShortDoctype: true, 9 | minifyCSS: true 10 | } 11 | 12 | module.exports = (html, options) => 13 | minify(html, options === true ? defaultOptions : options) 14 | -------------------------------------------------------------------------------- /lib/utils/prettyPath.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = p => path.relative(process.cwd(), p) 4 | -------------------------------------------------------------------------------- /lib/utils/renderHtml.js: -------------------------------------------------------------------------------- 1 | const serialize = require('serialize-javascript') 2 | const defaultDocument = require('./document') 3 | 4 | module.exports = async (renderer, context) => { 5 | const reamRootHtml = await renderer.renderToString(context) 6 | 7 | const document = context.document || defaultDocument 8 | const { title, link, style, script, noscript, meta } = context.meta.inject() 9 | 10 | let html = 11 | '' + 12 | document({ 13 | data: context.documentData, 14 | headTags({ resourceHints = true } = {}) { 15 | return ( 16 | `${meta.text()} 17 | ${title.text()} 18 | ${link.text()} 19 | ${style.text()} 20 | ${script.text()} 21 | ${noscript.text()}` + 22 | (resourceHints ? context.renderResourceHints() : '') + 23 | context.renderStyles() 24 | ) 25 | }, 26 | scripts() { 27 | return ( 28 | `` + context.renderScripts() 31 | ) 32 | } 33 | }) 34 | 35 | // TODO: instead of replace, just call document({ html: reamRootHtml }) 36 | // This will be a breaking API change. 37 | html = html.replace('', reamRootHtml) 38 | 39 | return html 40 | } 41 | -------------------------------------------------------------------------------- /lib/utils/serveStatic.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | module.exports = (path, cache) => 4 | express.static(path, { 5 | maxAge: cache ? '1d' : 0, 6 | dotfiles: 'allow' 7 | }) 8 | -------------------------------------------------------------------------------- /lib/utils/setupWebpackMiddlewares.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | module.exports = api => { 4 | const [serverCompiler, clientCompiler] = api.createCompilers() 5 | 6 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 7 | publicPath: clientCompiler.options.output.publicPath, 8 | logLevel: 'silent' 9 | }) 10 | 11 | let clientManifest 12 | let serverBundle 13 | 14 | const notify = () => { 15 | if (clientManifest && serverBundle) { 16 | api.createRenderer({ 17 | clientManifest, 18 | serverBundle, 19 | serverType: 'dev' 20 | }) 21 | } 22 | } 23 | 24 | const mfs = new webpack.MemoryOutputFileSystem() 25 | serverCompiler.outputFileSystem = mfs 26 | serverCompiler.watch({}, err => { 27 | if (err) console.error(err) 28 | serverBundle = JSON.parse( 29 | mfs.readFileSync(api.resolveOutDir('server/server-bundle.json'), 'utf8') 30 | ) 31 | notify() 32 | }) 33 | 34 | clientCompiler.plugin('done', stats => { 35 | if (!stats.hasErrors()) { 36 | clientManifest = JSON.parse( 37 | clientCompiler.outputFileSystem.readFileSync( 38 | api.resolveOutDir('client/client-manifest.json'), 39 | 'utf8' 40 | ) 41 | ) 42 | notify() 43 | } 44 | }) 45 | 46 | const hotMiddleware = require('webpack-hot-middleware')(clientCompiler, { 47 | log: false 48 | }) 49 | 50 | return server => { 51 | server.use(devMiddleware) 52 | server.use(hotMiddleware) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/validateConfig.js: -------------------------------------------------------------------------------- 1 | const joi = require('joi') 2 | 3 | const schema = joi.object().keys({ 4 | entry: joi.string(), 5 | outDir: joi.string(), 6 | fsRoutes: joi.alternatives().try([ 7 | joi.object().keys({ 8 | baseDir: joi.string(), 9 | basePath: joi.string(), 10 | match: joi.object().type(RegExp) 11 | }), 12 | joi.boolean() 13 | ]), 14 | transpileDependencies: joi.array().items(joi.string()), 15 | runtimeCompiler: joi.boolean(), 16 | productionSourceMap: joi.boolean(), 17 | chainWebpack: joi.func(), 18 | configureWebpack: joi.alternatives().try(joi.object(), joi.func()), 19 | server: joi.object().keys({ 20 | port: joi.number(), 21 | host: joi.string() 22 | }), 23 | plugins: joi.array().items( 24 | joi.object().keys({ 25 | name: joi.string().required(), 26 | apply: joi.func().required() 27 | }) 28 | ), 29 | generate: joi.object().keys({ 30 | routes: joi.array().items(joi.string()) 31 | }), 32 | css: joi.object().keys({ 33 | extract: joi.boolean() 34 | }), 35 | // In the future this option could also be an object 36 | // To add more controls, e.g. "notifyOnUpdate" will show a notifier when a new update is available 37 | pwa: joi.boolean(), 38 | minimize: joi.boolean(), 39 | defaultBabelPreset: joi.any().valid(['minimal', false]), 40 | minifyHtml: joi.alternatives().try(joi.object(), joi.boolean()) 41 | }) 42 | 43 | module.exports = config => { 44 | return schema.validate(config, { 45 | convert: false 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /lib/validateConfig.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const validateConfig = require('./validateConfig') 3 | 4 | tap.doesNotThrow(() => { 5 | const res = validateConfig({ 6 | entry: 'hi', 7 | server: { 8 | port: 7000 9 | } 10 | }) 11 | if (res.error) { 12 | throw res.error 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /lib/webpack/loaders/ream-babel-loader.js: -------------------------------------------------------------------------------- 1 | const babelLoader = require('babel-loader') 2 | const logger = require('../../logger') 3 | 4 | module.exports = babelLoader.custom(babel => { 5 | const configs = new Set() 6 | 7 | return { 8 | customOptions(opts) { 9 | const custom = opts.reamPresetOptions 10 | delete opts.reamPresetOptions 11 | 12 | return { loader: opts, custom } 13 | }, 14 | config(cfg, { customOptions }) { 15 | const options = Object.assign({}, cfg.options) 16 | 17 | if (cfg.hasFilesystemConfig()) { 18 | for (const file of [cfg.babelrc, cfg.config]) { 19 | if (file && !configs.has(file)) { 20 | configs.add(file) 21 | logger.debug(`Using external babel config file: ${file}`) 22 | } 23 | } 24 | } 25 | 26 | const reamPresetItem = babel.createConfigItem( 27 | [require.resolve('../../babel/preset'), customOptions], 28 | { 29 | type: 'preset' 30 | } 31 | ) 32 | 33 | // Add our default preset 34 | options.presets = [reamPresetItem, ...options.presets] 35 | 36 | return options 37 | } 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /lib/webpack/plugins/WatchMissingNodeModulesPlugin.js: -------------------------------------------------------------------------------- 1 | class WatchMissingNodeModulesPlugin { 2 | constructor(nodeModulesPath) { 3 | this.nodeModulesPath = nodeModulesPath 4 | } 5 | 6 | apply(compiler) { 7 | compiler.hooks.emit.tap('emit', compilation => { 8 | const missingDeps = compilation.missingDependencies 9 | const nodeModulesPath = this.nodeModulesPath 10 | 11 | // If any missing files are expected to appear in node_modules... 12 | if ([...missingDeps].some(file => file.indexOf(nodeModulesPath) !== -1)) { 13 | // ...tell webpack to watch node_modules recursively until they appear. 14 | compilation.contextDependencies.add(nodeModulesPath) 15 | } 16 | }) 17 | } 18 | } 19 | 20 | WatchMissingNodeModulesPlugin.__expression = `require('ream/lib/webpack/WatchMissingNodeModulesPlugin')` 21 | 22 | module.exports = WatchMissingNodeModulesPlugin 23 | -------------------------------------------------------------------------------- /lib/webpack/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const chalk = require('chalk') 4 | const TimeFixPlugin = require('time-fix-plugin') 5 | const { ownDir, inWorkspace } = require('../utils/dir') 6 | 7 | const resolveModules = config => { 8 | const modules = [ 9 | path.resolve('node_modules'), 10 | inWorkspace ? ownDir('../../node_modules') : ownDir('node_modules'), 11 | 'node_modules' 12 | ] 13 | config.resolve.modules.merge(modules) 14 | config.resolveLoader.modules.merge(modules) 15 | config.resolve.set('symlinks', true) 16 | config.resolveLoader.set('symlinks', true) 17 | config.resolveLoader.set('alias', { 18 | 'vue-loader': require.resolve('vue-loader') 19 | }) 20 | } 21 | 22 | module.exports = (api, config, type) => { 23 | config.devtool( 24 | api.options.dev 25 | ? 'cheap-module-source-map' 26 | : api.config.productionSourceMap 27 | ? 'source-map' 28 | : false 29 | ) 30 | 31 | config.resolve.alias 32 | .set('#app-entry$', api.resolveBaseDir(api.config.entry)) 33 | .set('#base', api.resolveBaseDir()) 34 | .set('#out', api.resolveOutDir()) 35 | .set('#app', ownDir('app')) 36 | 37 | if (api.config.runtimeCompiler) { 38 | config.resolve.alias.set('vue$', 'vue/dist/vue.esm.js') 39 | } 40 | 41 | config.entry(type).add(ownDir(`app/${type}-entry.js`)) 42 | 43 | // Add HMR support 44 | if (type === 'client' && api.options.dev) { 45 | config.entry(type).prepend(require.resolve('webpack-hot-middleware/client')) 46 | config.plugin('hmr').use(webpack.HotModuleReplacementPlugin) 47 | } 48 | 49 | const publicPath = '/_ream/' 50 | const filename = api.options.dev ? '[name].js' : '[name].[chunkhash:8].js' 51 | config.merge({ 52 | mode: api.options.dev ? 'development' : 'production', 53 | performance: { 54 | hints: false 55 | }, 56 | output: { 57 | filename, 58 | chunkFilename: filename, 59 | publicPath 60 | }, 61 | optimization: { 62 | minimize: false 63 | } 64 | }) 65 | 66 | // No need to minimize in server or dev mode 67 | if (type === 'client' && !api.options.dev && api.config.minimize !== false) { 68 | config.merge({ 69 | optimization: { 70 | minimize: true, 71 | minimizer: [ 72 | { 73 | apply(compiler) { 74 | const TerserPlugin = require('terser-webpack-plugin') 75 | new TerserPlugin({ 76 | cache: true, 77 | parallel: true, 78 | sourceMap: config.get('devtool') !== false, 79 | terserOptions: { 80 | parse: { 81 | // we want terser to parse ecma 8 code. However, we don't want it 82 | // to apply any minfication steps that turns valid ecma 5 code 83 | // into invalid ecma 5 code. This is why the 'compress' and 'output' 84 | // sections only apply transformations that are ecma 5 safe 85 | // https://github.com/facebook/create-react-app/pull/4234 86 | ecma: 8 87 | }, 88 | compress: { 89 | ecma: 5, 90 | warnings: false, 91 | // Disabled because of an issue with Uglify breaking seemingly valid code: 92 | // https://github.com/facebook/create-react-app/issues/2376 93 | // Pending further investigation: 94 | // https://github.com/mishoo/UglifyJS2/issues/2011 95 | comparisons: false, 96 | // Disabled because of an issue with Terser breaking valid code: 97 | // https://github.com/facebook/create-react-app/issues/5250 98 | // Pending futher investigation: 99 | // https://github.com/terser-js/terser/issues/120 100 | // Fixed in terser 3.10.7 which is not yet pulled by terser-webpack-plugin 101 | inline: 2 102 | }, 103 | mangle: { 104 | safari10: true 105 | }, 106 | output: { 107 | ecma: 5, 108 | comments: false, 109 | // Turned on because emoji and regex is not minified properly using default 110 | // https://github.com/facebook/create-react-app/issues/2488 111 | ascii_only: true // eslint-disable-line camelcase 112 | } 113 | } 114 | }).apply(compiler) 115 | } 116 | }, 117 | { 118 | apply(compiler) { 119 | // eslint-disable-next-line import/no-extraneous-dependencies 120 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 121 | new OptimizeCSSAssetsPlugin().apply(compiler) 122 | } 123 | } 124 | ] 125 | } 126 | }) 127 | } 128 | 129 | // prettier-ignore 130 | webpack.DefinePlugin.__expression = 'webpack.DefinePlugin' 131 | config.plugin('constants').use(webpack.DefinePlugin, [ 132 | { 133 | 'process.env.NODE_ENV': JSON.stringify( 134 | api.options.dev ? 'development' : 'production' 135 | ), 136 | 'process.server': type === 'server', 137 | 'process.browser': type === 'client', 138 | 'process.client': type === 'client', 139 | __DEV__: Boolean(api.options.dev), 140 | __PUBLIC_PATH__: JSON.stringify(publicPath) 141 | } 142 | ]) 143 | 144 | resolveModules(config) 145 | 146 | const babelOptions = { 147 | cacheDirectory: true, 148 | reamPresetOptions: { 149 | isServer: type === 'server', 150 | dev: api.options.dev, 151 | defaultBabelPreset: api.config.defaultBabelPreset 152 | } 153 | } 154 | 155 | // prettier-ignore 156 | config.module.rule('js') 157 | .test(/\.js$/) 158 | .include 159 | .add(filepath => { 160 | // Transpile enhanceAppFiles 161 | if ([...api.enhanceAppFiles].some(p => filepath.startsWith(p))) { 162 | return true 163 | } 164 | // Ream's own app 165 | if (filepath.startsWith(ownDir('app'))) { 166 | return true 167 | } 168 | const shouldTranspileDeps = api.config.transpileDependencies.some(dep => { 169 | return filepath.includes(path.normalize(`/node_modules/${dep}/`)) 170 | }) 171 | if (shouldTranspileDeps) { 172 | return true 173 | } 174 | return !/node_modules/.test(filepath) 175 | }) 176 | .end() 177 | .use('babel-loader') 178 | .loader(require.resolve('./loaders/ream-babel-loader')) 179 | .options(babelOptions) 180 | 181 | // prettier-ignore 182 | config.module 183 | .rule('vue') 184 | .test(/\.vue$/) 185 | .use('vue-loader') 186 | .loader('vue-loader') 187 | 188 | const { VueLoaderPlugin } = require('vue-loader') 189 | VueLoaderPlugin.__expression = `require('vue-loader').VueLoaderPlugin` 190 | config.plugin('vue').use(VueLoaderPlugin) 191 | 192 | const inlineLimit = 10000 193 | 194 | // prettier-ignore 195 | config.module 196 | .rule('images') 197 | .test(/\.(png|jpe?g|gif)(\?.*)?$/) 198 | .use('url-loader') 199 | .loader('url-loader') 200 | .options({ 201 | limit: inlineLimit, 202 | name: `assets/img/[name].[hash:8].[ext]` 203 | }) 204 | 205 | // do not base64-inline SVGs. 206 | // https://github.com/facebookincubator/create-react-app/pull/1180 207 | // prettier-ignore 208 | config.module 209 | .rule('svg') 210 | .test(/\.(svg)(\?.*)?$/) 211 | .use('file-loader') 212 | .loader('file-loader') 213 | .options({ 214 | name: `assets/img/[name].[hash:8].[ext]` 215 | }) 216 | 217 | // prettier-ignore 218 | config.module 219 | .rule('media') 220 | .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/) 221 | .use('url-loader') 222 | .loader('url-loader') 223 | .options({ 224 | limit: inlineLimit, 225 | name: `assets/media/[name].[hash:8].[ext]` 226 | }) 227 | 228 | // prettier-ignore 229 | config.module 230 | .rule('fonts') 231 | .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i) 232 | .use('url-loader') 233 | .loader('url-loader') 234 | .options({ 235 | limit: inlineLimit, 236 | name: `assets/fonts/[name].[hash:8].[ext]` 237 | }) 238 | 239 | const isProd = !api.options.dev 240 | const { extract } = api.config.css 241 | 242 | if (extract && type === 'client') { 243 | const cssFilename = filename.replace(/\.js$/, '.css') 244 | config.plugin('extract-css').use(require('mini-css-extract-plugin'), [ 245 | { 246 | filename: cssFilename, 247 | chunkFilename: cssFilename 248 | } 249 | ]) 250 | } 251 | 252 | function createCSSRule(lang, test, loader, options) { 253 | const baseRule = config.module.rule(lang).test(test) 254 | const modulesRule = baseRule.oneOf('modules').resourceQuery(/module/) 255 | const normalRule = baseRule.oneOf('normal') 256 | 257 | applyLoaders(modulesRule, true) 258 | applyLoaders(normalRule, false) 259 | 260 | function applyLoaders(rule, modules) { 261 | const sourceMap = config.get('devtool') !== false 262 | 263 | if (extract) { 264 | if (type === 'client') { 265 | rule 266 | .use('extract-loader') 267 | .loader(require('mini-css-extract-plugin').loader) 268 | } 269 | } else { 270 | rule.use('vue-style-loader').loader('vue-style-loader') 271 | } 272 | 273 | rule 274 | .use('css-loader') 275 | .loader('css-loader') 276 | .options({ 277 | modules, 278 | sourceMap, 279 | localIdentName: `[local]_[hash:base64:8]`, 280 | importLoaders: 0 + Boolean(api.config.postcss) + Boolean(loader) 281 | }) 282 | 283 | // Only use postcss-loader when a config file was found 284 | if (api.config.postcss) { 285 | rule 286 | .use('postcss-loader') 287 | .loader('postcss-loader') 288 | .options( 289 | Object.assign( 290 | { 291 | sourceMap: !isProd 292 | }, 293 | api.config.postcss 294 | ) 295 | ) 296 | } 297 | 298 | if (loader) { 299 | rule 300 | .use(loader) 301 | .loader(loader) 302 | .options( 303 | Object.assign( 304 | { 305 | sourceMap 306 | }, 307 | options 308 | ) 309 | ) 310 | } 311 | } 312 | } 313 | 314 | createCSSRule('css', /\.css$/) 315 | createCSSRule('scss', /\.scss$/, 'sass-loader') 316 | createCSSRule('sass', /\.sass$/, 'sass-loader', { indentedSyntax: true }) 317 | createCSSRule('less', /\.less$/, 'less-loader') 318 | createCSSRule('stylus', /\.styl(us)?$/, 'stylus-loader', { 319 | preferPathResolver: 'webpack' 320 | }) 321 | 322 | // prettier-ignore 323 | TimeFixPlugin.__expression = `require('time-fix-plugin')` 324 | config.plugin('timefix').use(TimeFixPlugin) 325 | 326 | config 327 | .plugin('watch-missing') 328 | .use(require('./plugins/WatchMissingNodeModulesPlugin')) 329 | 330 | if ( 331 | api.options.progress !== false && 332 | !api.options.debug && 333 | !api.options.inspectWebpack 334 | ) { 335 | const webpackbar = require('webpackbar') 336 | webpackbar.__expression = `require('webpackbar')` 337 | config.plugin('webpackbar').use(webpackbar, [ 338 | { 339 | name: type, 340 | color: type === 'server' ? 'green' : 'magenta' 341 | } 342 | ]) 343 | } 344 | 345 | config.plugin('report').use( 346 | class ReportPlugin { 347 | apply(compiler) { 348 | compiler.hooks.invalid.tap('report-change', (filename, changeTime) => { 349 | const d = new Date(changeTime) 350 | console.log( 351 | chalk.dim( 352 | `[${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}] Rebuilding due to changes made in ${chalk.cyan( 353 | path.relative(process.cwd(), filename) 354 | )}` 355 | ) 356 | ) 357 | }) 358 | compiler.hooks.done.tap('report-status', stats => { 359 | if (stats.hasErrors() || stats.hasWarnings()) { 360 | console.log( 361 | stats.toString({ 362 | colors: true, 363 | children: false, 364 | modules: false, 365 | assets: false 366 | }) 367 | ) 368 | } 369 | }) 370 | } 371 | } 372 | ) 373 | } 374 | -------------------------------------------------------------------------------- /lib/webpack/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 2 | const baseConfig = require('./webpack.config.base') 3 | 4 | VueSSRClientPlugin.__expression = `require('vue-server-renderer/client-plugin')` 5 | 6 | module.exports = (api, config) => { 7 | baseConfig(api, config, 'client') 8 | 9 | config.merge({ 10 | output: { 11 | path: api.resolveOutDir('client') 12 | } 13 | }) 14 | 15 | // Vue SSR plugin 16 | config.plugin('ssr').use(VueSSRClientPlugin, [ 17 | { 18 | filename: 'client-manifest.json' 19 | } 20 | ]) 21 | 22 | if (!api.options.dev) { 23 | // Code splitting, only for client bundle 24 | config.optimization.splitChunks({ 25 | chunks: 'all', 26 | name: (m, chunks, cacheGroup) => `chunk-${cacheGroup}`, 27 | cacheGroups: { 28 | vendors: { 29 | test: /[\\/]node_modules[\\/]/, 30 | priority: -10 31 | }, 32 | common: { 33 | minChunks: 2, 34 | priority: -20, 35 | reuseExistingChunk: true 36 | } 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/webpack/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 2 | const nodeExternals = require('webpack-node-externals') 3 | const baseConfig = require('./webpack.config.base') 4 | 5 | VueSSRServerPlugin.__expression = `require('vue-server-renderer/server-plugin')` 6 | 7 | module.exports = (api, config) => { 8 | baseConfig(api, config, 'server') 9 | 10 | config.merge({ 11 | output: { 12 | libraryTarget: 'commonjs2', 13 | path: api.resolveOutDir('server') 14 | }, 15 | target: 'node' 16 | }) 17 | 18 | // Vue SSR plugin 19 | config.plugin('ssr').use(VueSSRServerPlugin, [ 20 | { 21 | filename: 'server-bundle.json' 22 | } 23 | ]) 24 | 25 | config.externals([ 26 | 'vue', 27 | 'vuex', 28 | 'vue-router', 29 | 'vue-meta', 30 | nodeExternals({ 31 | whitelist: [/\.(?!(?:js|json)$).{1,5}(\?.+)?$/i] 32 | }) 33 | ]) 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ream", 3 | "version": "0.0.0-managed-by-semantic-release", 4 | "description": "Framework for building unversal web apps.", 5 | "repository": { 6 | "url": "https://github.com/ream/ream.git", 7 | "type": "git" 8 | }, 9 | "bin": "bin/cli.js", 10 | "main": "lib/index.js", 11 | "types": "lib/index.d.ts", 12 | "scripts": { 13 | "test": "npm run lint && npm run tap", 14 | "tap": "tap --jobs-auto '{{lib,test}/**/*.test.js,examples/*/*.test.js}'", 15 | "lint": "xo", 16 | "postinstall": "node -e \"console.log('\\u001b[35m\\u001b[1mLove Ream? You can now donate to support the author:\\u001b[22m\\u001b[39m\\n> \\u001b[34mhttps://patreon.com/egoist\\u001b[0m')\"" 17 | }, 18 | "files": [ 19 | "lib", 20 | "bin", 21 | "app", 22 | "webpack.config.js", 23 | "!**/__test__/**", 24 | "!**/*.test.js" 25 | ], 26 | "engines": { 27 | "node": ">=8", 28 | "npm": ">=5" 29 | }, 30 | "author": "egoist <0x142857@gmail.com>", 31 | "license": "MIT", 32 | "dependencies": { 33 | "@babel/core": "^7.0.0-beta.47", 34 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.47", 35 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.47", 36 | "@babel/plugin-syntax-dynamic-import": "^7.0.0-beta.47", 37 | "@babel/plugin-syntax-jsx": "^7.0.0-beta.47", 38 | "@babel/plugin-transform-runtime": "^7.0.0-beta.47", 39 | "@babel/preset-env": "^7.0.0-beta.47", 40 | "@babel/runtime": "^7.0.0-beta.47", 41 | "@types/express": "^4.16.0", 42 | "@types/webpack": "^4.4.20", 43 | "@types/webpack-chain": "^4.8.0", 44 | "async-to-gen": "^1.4.0", 45 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 46 | "babel-loader": "^8.0.0-beta.3", 47 | "babel-plugin-transform-vue-jsx": "^4.0.0", 48 | "babel-plugin-webpack-chunkname": "^1.2.0", 49 | "cac": "^4.4.1", 50 | "chalk": "^2.3.1", 51 | "chokidar": "^2.0.3", 52 | "compression": "^1.7.2", 53 | "css-loader": "^1.0.0", 54 | "express": "^4.16.3", 55 | "file-loader": "^1.1.6", 56 | "fs-extra": "^6.0.0", 57 | "hash-sum": "^1.0.2", 58 | "html-minifier": "^4.0.0", 59 | "internal-ip": "^3.0.1", 60 | "joi": "^13.6.0", 61 | "joycon": "^1.0.4", 62 | "lodash.debounce": "^4.0.8", 63 | "lodash.merge": "^4.6.1", 64 | "lodash.sortby": "^4.7.0", 65 | "log-update": "^2.3.0", 66 | "mini-css-extract-plugin": "^0.4.1", 67 | "object-assign": "^4.1.1", 68 | "optimize-css-assets-webpack-plugin": "^5.0.0", 69 | "postcss-loader": "^3.0.0", 70 | "promise-mutex": "^0.1.1", 71 | "promise-polyfill": "^7.1.2", 72 | "register-service-worker": "^1.2.0", 73 | "serialize-javascript": "^1.4.0", 74 | "serve-static": "^1.13.2", 75 | "terser-webpack-plugin": "^1.1.0", 76 | "time-fix-plugin": "^2.0.4", 77 | "url-loader": "^1.1.0", 78 | "vue": "^2.5.16", 79 | "vue-loader": "^15.0.11", 80 | "vue-meta": "^1.5.2", 81 | "vue-router": "^3.0.1", 82 | "vue-server-renderer": "^2.5.16", 83 | "vue-template-compiler": "^2.5.16", 84 | "webpack": "^4.26.1", 85 | "webpack-chain": "^4.8.0", 86 | "webpack-dev-middleware": "^3.1.3", 87 | "webpack-hot-middleware": "^2.22.2", 88 | "webpack-node-externals": "^1.7.2", 89 | "webpackbar": "^2.6.1", 90 | "workbox-build": "^3.2.0" 91 | }, 92 | "xo": { 93 | "extends": [ 94 | "rem", 95 | "plugin:prettier/recommended" 96 | ], 97 | "rules": { 98 | "unicorn/filename-case": "off", 99 | "import/prefer-default-export": "off", 100 | "unicorn/no-abusive-eslint-disable": "off", 101 | "prefer-destructuring": "off" 102 | }, 103 | "globals": [ 104 | "__DEV__" 105 | ], 106 | "ignores": [ 107 | "**/examples/**", 108 | "tap-snapshots/**" 109 | ] 110 | }, 111 | "keywords": [ 112 | "ssr", 113 | "vue", 114 | "universal", 115 | "static", 116 | "nuxt", 117 | "next", 118 | "ream", 119 | "server-side", 120 | "framework" 121 | ], 122 | "devDependencies": { 123 | "@commitlint/cli": "^7.2.1", 124 | "@commitlint/config-conventional": "^7.1.2", 125 | "axios": "^0.18.0", 126 | "eslint-config-prettier": "^2.9.0", 127 | "eslint-config-rem": "^4.0.0", 128 | "eslint-plugin-prettier": "^2.6.0", 129 | "husky": "^1.3.1", 130 | "lerna": "^2.11.0", 131 | "lint-staged": "^8.1.0", 132 | "prettier": "^1.12.1", 133 | "puppeteer": "^1.9.0", 134 | "semantic-release": "^15.9.17", 135 | "tap": "^13.1.6", 136 | "vuex": "^3.0.1", 137 | "xo": "^0.20.1" 138 | }, 139 | "husky": { 140 | "hooks": { 141 | "pre-commit": "lint-staged", 142 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 143 | } 144 | }, 145 | "lint-staged": { 146 | "linters": { 147 | "*.js": [ 148 | "xo --fix", 149 | "git add" 150 | ] 151 | }, 152 | "relative": true 153 | }, 154 | "commitlint": { 155 | "extends": [ 156 | "@commitlint/config-conventional" 157 | ] 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tap-snapshots/examples-custom-root-component-index.test.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`examples/custom-root-component/index.test.js TAP examples/custom-root-component > page 1`] = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 |

custom root component

homepage

27 | 28 | 29 | 30 | ` 31 | -------------------------------------------------------------------------------- /tap-snapshots/examples-fs-routes-index.test.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`examples/fs-routes/index.test.js TAP examples/fs-routes > / 1`] = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

fs-routes example app

Users
23 | 24 | 25 | 26 | ` 27 | 28 | exports[`examples/fs-routes/index.test.js TAP examples/fs-routes > /user 1`] = ` 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ` 47 | 48 | exports[`examples/fs-routes/index.test.js TAP examples/fs-routes > /user/mary 1`] = ` 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |

Users

mary

User mary profile
63 | 64 | 65 | 66 | ` 67 | -------------------------------------------------------------------------------- /tap-snapshots/examples-prepopulate-vuex-index.test.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`examples/prepopulate-vuex/index.test.js TAP examples/prepopulate-vuex > page 1`] = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

hello1

23 | 24 | 25 | 26 | ` 27 | -------------------------------------------------------------------------------- /tap-snapshots/lib-plugins-fs-routes-__test__-routes.test.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`lib/plugins/fs-routes/__test__/routes.test.js TAP presets/empty > routes 1`] = ` 9 | Array [] 10 | ` 11 | 12 | exports[`lib/plugins/fs-routes/__test__/routes.test.js TAP presets/ignore-garbage > routes 1`] = ` 13 | Array [ 14 | Object { 15 | "path": "/page", 16 | "component": "#base/page.vue", 17 | }, 18 | ] 19 | ` 20 | 21 | exports[`lib/plugins/fs-routes/__test__/routes.test.js TAP presets/nested > routes 1`] = ` 22 | Array [ 23 | Object { 24 | "path": "/foo/bar/baz/qux", 25 | "component": "#base/foo/bar/baz/qux.vue", 26 | }, 27 | ] 28 | ` 29 | 30 | exports[`lib/plugins/fs-routes/__test__/routes.test.js TAP presets/nested-children > routes 1`] = ` 31 | Array [ 32 | Object { 33 | "path": "/foo", 34 | "component": "#base/foo.vue", 35 | "children": Array [ 36 | Object { 37 | "path": "bar", 38 | "component": "#base/foo/bar.vue", 39 | "children": Array [ 40 | Object { 41 | "path": "baz", 42 | "component": "#base/foo/bar/baz.vue", 43 | "children": Array [ 44 | Object { 45 | "path": "qux", 46 | "component": "#base/foo/bar/baz/qux.vue", 47 | }, 48 | ], 49 | }, 50 | ], 51 | }, 52 | ], 53 | }, 54 | ] 55 | ` 56 | 57 | exports[`lib/plugins/fs-routes/__test__/routes.test.js TAP presets/typical > routes 1`] = ` 58 | Array [ 59 | Object { 60 | "path": "/", 61 | "component": "#base/index.vue", 62 | }, 63 | Object { 64 | "path": "/foo", 65 | "component": "#base/foo.vue", 66 | }, 67 | Object { 68 | "path": "/user", 69 | "component": "#base/user.vue", 70 | "children": Array [ 71 | Object { 72 | "path": ":user", 73 | "component": "#base/user/[user].vue", 74 | "children": Array [ 75 | Object { 76 | "path": "", 77 | "component": "#base/user/[user]/index.vue", 78 | }, 79 | Object { 80 | "path": "friends", 81 | "component": "#base/user/[user]/friends.vue", 82 | }, 83 | ], 84 | }, 85 | ], 86 | }, 87 | ] 88 | ` 89 | 90 | exports[`lib/plugins/fs-routes/__test__/routes.test.js TAP special/custom-base-path > routes 1`] = ` 91 | Array [ 92 | Object { 93 | "path": "/some/foo", 94 | "component": "#base/foo.vue", 95 | }, 96 | ] 97 | ` 98 | 99 | exports[`lib/plugins/fs-routes/__test__/routes.test.js TAP special/typescript > routes 1`] = ` 100 | Array [ 101 | Object { 102 | "path": "/", 103 | "component": "#base/index.vue", 104 | }, 105 | Object { 106 | "path": "/page", 107 | "component": "#base/page.ts", 108 | }, 109 | ] 110 | ` 111 | -------------------------------------------------------------------------------- /tap-snapshots/test-projects-errors-index.test.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/projects/errors/index.test.js TAP test/projects/errors server-side 404 > must match snapshot 1`] = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

404: page not found

23 | 24 | 25 | 26 | ` 27 | -------------------------------------------------------------------------------- /tap-snapshots/test-projects-html-minifier-index.test.js-TAP.test.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/projects/html-minifier/index.test.js TAP test/projects/html-minifier > page 1`] = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 |
27 | Test 28 |
29 | 30 | 31 | 32 | ` 33 | 34 | exports[`test/projects/html-minifier/index.test.js TAP test/projects/html-minifier > page 2`] = ` 35 |
Test
36 | ` 37 | 38 | exports[`test/projects/html-minifier/index.test.js TAP test/projects/html-minifier > page 3`] = ` 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | Test 54 |
55 | 56 | 57 | 58 | ` 59 | -------------------------------------------------------------------------------- /test/lib/testProject.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const path = require('path') 3 | const url = require('url') 4 | const axios = require('axios') 5 | const puppeteer = require('puppeteer') 6 | const tap = require('tap') 7 | const ream = require('../..') 8 | 9 | class Client { 10 | constructor(baseURL) { 11 | this.baseURL = baseURL 12 | this.axios = axios.create({ baseURL }) 13 | } 14 | 15 | async loadPage(pageUrl) { 16 | if (!this.browser) { 17 | // NOTE: race condition here. 18 | // Please don't call loadPage() asynchronously. 19 | this.browser = await puppeteer.launch({ 20 | args: ['--no-sandbox', '--disable-setuid-sandbox'] 21 | }) 22 | } 23 | const page = await this.browser.newPage() 24 | await page.goto(this.resolveUrl(pageUrl)) 25 | return page 26 | } 27 | 28 | resolveUrl(pageUrl) { 29 | return url.resolve(this.baseURL, pageUrl) 30 | } 31 | 32 | async close() { 33 | if (this.browser) { 34 | await this.browser.close() 35 | } 36 | } 37 | } 38 | 39 | module.exports = function(baseDir, fn, config) { 40 | // Calculate relative base directory for consistent snapshots. 41 | const relativeBaseDir = path.relative('.', baseDir) 42 | return tap.test(relativeBaseDir, async t => { 43 | const app = ream( 44 | { 45 | baseDir, 46 | dev: false 47 | }, 48 | { 49 | css: { 50 | extract: false 51 | }, 52 | minimize: false, 53 | ...config 54 | } 55 | ) 56 | app.chainWebpack(config => { 57 | config.plugins.delete('webpackbar') 58 | // Ensure consistent filenames in different environments. 59 | const filename = '[name].js' 60 | config.merge({ 61 | output: { 62 | filename, 63 | chunkFilename: filename 64 | } 65 | }) 66 | }) 67 | await app.build() 68 | const handler = await app.getRequestHandler() 69 | const server = http.createServer(handler) 70 | server.listen(0) // Automatically pick a random free port. 71 | const port = server.address().port 72 | const baseURL = `http://127.0.0.1:${port}` 73 | const client = new Client(baseURL) 74 | try { 75 | await fn(t, client) 76 | } finally { 77 | await client.close() 78 | server.close() 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /test/projects/entry-middleware/index.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | async middleware(context) { 4 | context.type = context.req ? 'server' : 'client' 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/projects/entry-middleware/index.test.js: -------------------------------------------------------------------------------- 1 | const testProject = require('../../lib/testProject') 2 | 3 | testProject(__dirname, async (t, c) => { 4 | await t.test('entry middleware', async t => { 5 | const page = await c.loadPage('/') 6 | await page.waitForXPath('//strong[text()="server"]') 7 | await page.waitForSelector('a').then(e => e.click()) 8 | await page.waitForXPath('//strong[text()="client"]') 9 | t.pass() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/projects/entry-middleware/pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /test/projects/entry-middleware/ream.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fsRoutes: true 3 | } 4 | -------------------------------------------------------------------------------- /test/projects/errors/index.test.js: -------------------------------------------------------------------------------- 1 | const testProject = require('../../lib/testProject') 2 | 3 | testProject(__dirname, async (t, c) => { 4 | await t.test('server-side 404', async t => { 5 | const res = await c.axios.get('/not-found', { 6 | validateStatus: status => status === 404 7 | }) 8 | t.matchSnapshot(res.data) 9 | }) 10 | 11 | await t.test('client-side 404', async t => { 12 | const page = await c.loadPage('/') 13 | await page.waitForXPath('//a[text()="Not Found"]').then(el => el.click()) 14 | await page 15 | .waitForSelector('#_ream') 16 | .then(el => page.evaluate(el => el.innerText, el)) 17 | .then(text => t.equal(text, '404: page not found')) 18 | }) 19 | 20 | await t.test('server-side unhandled error', async t => { 21 | const res = await c.axios.get('/error', { 22 | validateStatus: status => status === 500 23 | }) 24 | t.equal(res.data, 'server error') 25 | }) 26 | 27 | // Don't test unhandled client-side error for now, as it's (ha-ha) not handled. 28 | }) 29 | -------------------------------------------------------------------------------- /test/projects/errors/pages/error.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /test/projects/errors/pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /test/projects/errors/ream.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fsRoutes: true 3 | } 4 | -------------------------------------------------------------------------------- /test/projects/html-minifier/index.test.js: -------------------------------------------------------------------------------- 1 | const testProject = require('../../lib/testProject') 2 | 3 | const configs = [ 4 | { 5 | minifyHtml: false 6 | }, 7 | { 8 | minifyHtml: true 9 | }, 10 | { 11 | minifyHtml: { 12 | minifyCSS: true 13 | } 14 | } 15 | ] 16 | 17 | for (const config of configs) { 18 | testProject( 19 | __dirname, 20 | async (t, c) => { 21 | t.matchSnapshot((await c.axios.get('/')).data, 'page') 22 | }, 23 | config 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /test/projects/html-minifier/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test/projects/html-minifier/ream.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fsRoutes: true 3 | } 4 | -------------------------------------------------------------------------------- /test/projects/redirect/index.test.js: -------------------------------------------------------------------------------- 1 | const testProject = require('../../lib/testProject') 2 | 3 | testProject(__dirname, async (t, c) => { 4 | await t.test('server-side redirect', async t => { 5 | const res = await c.axios.get('/2', { 6 | maxRedirects: 0, 7 | validateStatus: status => status >= 300 && status < 400 8 | }) 9 | t.equal(res.headers.location, '/3') 10 | }) 11 | 12 | await t.test('client-side redirect', async t => { 13 | const page = await c.loadPage('/1') 14 | await page.waitForXPath('//a[text()="Page 2"]').then(e => e.click()) 15 | await page 16 | .waitForSelector('#target') 17 | .then(el => page.evaluate(el => el.innerText, el)) 18 | .then(text => t.equal(text, 'Page 3')) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/projects/redirect/pages/1.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /test/projects/redirect/pages/2.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /test/projects/redirect/pages/3.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/projects/redirect/ream.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fsRoutes: true 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // this might be useful for eslint-plugin-import 2 | const app = require('.')() 3 | 4 | module.exports = app.createConfigs()[0] 5 | 6 | /* example .eslintrc 7 | { 8 | "settings": { 9 | "import/resolver": { 10 | "webpack": { 11 | "config": "./node_modules/ream/webpack.config.js" 12 | } 13 | } 14 | } 15 | } 16 | */ 17 | --------------------------------------------------------------------------------