├── .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 |

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 |
2 |
3 |
custom root component
4 |
5 |
6 |
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 |
2 |
3 |
fs-routes example app
4 | Users
5 |
6 |
7 |
--------------------------------------------------------------------------------
/examples/fs-routes/pages/user.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Users
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/examples/fs-routes/pages/user/[user_id].vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | - Profile
6 | - Friends
7 |
8 |
9 |
10 |
11 |
12 |
21 |
--------------------------------------------------------------------------------
/examples/fs-routes/pages/user/[user_id]/friends.vue:
--------------------------------------------------------------------------------
1 |
2 | User {{ $route.params.user_id }} friends
3 |
4 |
--------------------------------------------------------------------------------
/examples/fs-routes/pages/user/[user_id]/index.vue:
--------------------------------------------------------------------------------
1 |
2 | User {{ $route.params.user_id }} profile
3 |
4 |
--------------------------------------------------------------------------------
/examples/fs-routes/pages/user/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 | {{ post.title }}
6 |
7 |
8 |
9 |
10 |
11 |
31 |
--------------------------------------------------------------------------------
/examples/with-apollo/views/post.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ post.title }}
4 |
5 | Votes: {{ post.votes }}
6 |
7 |
8 | Go Home ->
9 |
10 |
11 |
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 |
2 |
3 | -
4 | You've logged in!
5 |
6 | -
7 | Login
8 |
9 | -
10 | Secret page
11 |
12 |
13 |
14 |
15 |
31 |
--------------------------------------------------------------------------------
/examples/with-auth/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
37 |
--------------------------------------------------------------------------------
/examples/with-auth/views/Secret.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Top secret! This is your token: {{ token }}
4 |
5 |
6 |
7 |
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 |
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 |
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 |
29 |
30 |
31 |
32 | `
33 |
34 | exports[`test/projects/html-minifier/index.test.js TAP test/projects/html-minifier > page 2`] = `
35 |
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 |
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 |
2 |
3 | Page loaded from {{ type }}
4 | reload
5 |
6 |
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 |
2 | test
3 |
4 |
5 |
13 |
--------------------------------------------------------------------------------
/test/projects/errors/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Not Found
4 | Error
5 |
6 |
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 |
2 |
3 | Test
4 |
5 |
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 |
2 |
3 |
Page 1
4 | Page 2
5 |
6 |
7 |
--------------------------------------------------------------------------------
/test/projects/redirect/pages/2.vue:
--------------------------------------------------------------------------------
1 |
2 | Page 2: Should not see me
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/test/projects/redirect/pages/3.vue:
--------------------------------------------------------------------------------
1 |
2 | Page 3
3 |
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 |
--------------------------------------------------------------------------------