1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-25 07:52:52 +03:00

Front/structure refactor and building reconfigure (#277)

* DefferedPromise refactor

* Moved defferedPromise to utils folder

* Base url moved to app file

* Store modules states rewrittten according to vue doc recommendations

* Deffered promise usage moved

* removed unused packages in entries

* Structure refactor, easier building, prod building configured, tsconfig reconfigure removed useless packages

* Update front/index.html

Co-Authored-By: avele <34437766+avele@users.noreply.github.com>

* Update front/postcss.config.js

Co-Authored-By: avele <34437766+avele@users.noreply.github.com>

* Comment rewritten
This commit is contained in:
avele 2019-04-18 01:52:43 +04:00 committed by GitHub
parent 3c74057616
commit 469b5b5790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2080 additions and 4948 deletions

View File

@ -1,21 +0,0 @@
{
"presets": [
["@babel/env", {
"targets": {
"browsers": ["last 3 versions", "> 2%", "ie >= 10", "Firefox >= 30", "Chrome >= 30"]
},
"modules": false,
"loose": true,
"useBuiltIns": "entry"
}]
],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
],
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"]
}
}
}

View File

@ -1,9 +1,16 @@
## Commands ## Commands
- `client:dev` - Start developing environment of client content. - `dev` - Start developing environment (starts both server and setup client middleware for server).
- `client:build` - Build client content. - `build` - Builds to /dist folder. Compiles (from ts to js) server right in dist folder, client files compiled in /dist/src.
- `server:dev` - Start developing environment of ssr server.
- `server:build` - Build ssr content. ## Deploy process
- `build-all` - Run `client:build` and `server:build` at same time. Please run this command for deployment. Node version >= 11, because used fs async methods which experimental now
- `start:dev` - Run `client:dev` and `server:dev` at same time. Please run this command for development.
- `start:prod` - Start ssr server in production environment. You should run `build-all` first to make it works. - `git pull`
- `npm i`
- `npm run build`
- `set NODE_ENV=production`, also you can specify port by `set PORT=%port_number%`
- `cd dist`
- `npm i`
- `node run server`

21
front/babel.config.js Normal file
View File

@ -0,0 +1,21 @@
module.exports = {
presets: [
['@babel/env', {
targets: {
browsers: ['last 3 versions', '> 2%', 'ie >= 10', 'Firefox >= 30', 'Chrome >= 30']
},
modules: false,
loose: true,
useBuiltIns: 'entry'
}]
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-syntax-dynamic-import'
],
env: {
test: {
plugins: ['transform-es2015-modules-commonjs']
}
}
}

View File

@ -1,27 +0,0 @@
const path = require('path')
const moment = require('moment')
const axios = require('axios')
const appName = 'Aelve Guide'
const clientPort = 4000
const ssrPort = 5000
const distPath = rootResolve('./dist')
axios.defaults.baseURL = `http://localhost:${ssrPort}`
const env = {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.BUILD_TIME': JSON.stringify(moment(new Date()).format('YYYY-MM-DD HH:mm:ss'))
}
module.exports = {
appName,
env,
clientPort,
ssrPort,
distPath
}
function rootResolve (filePath) {
return path.resolve(__dirname, '../', filePath)
}

View File

@ -0,0 +1,127 @@
import fs from 'fs'
import path from 'path'
import MemoryFs from 'memory-fs'
import webpack from 'webpack'
import { createBundleRenderer } from 'vue-server-renderer'
import koaWebpack from 'koa-webpack'
import DeferredPromise from '../utils/DeferredPromise'
const fsAsync = fs.promises
MemoryFs.prototype.readFileAsync = async function (...args: any) {
return new Promise((resolve, reject) => {
this.readFile(...args, (err: any, result: string | Buffer) => {
if (err) {
reject(err)
return
}
resolve(result)
})
})
}
import serverConfig from '../build/webpack.server.conf'
import clientConfig from '../build/webpack.client.conf'
const serverCompiler = webpack(serverConfig)
const clientCompiler = webpack(clientConfig)
const mfs = new MemoryFs()
const clientManifestFileName = 'vue-ssr-client-manifest.json'
const serverBundleFileName = 'vue-ssr-server-bundle.json'
// TODO add icon and refactor, change favicon serving
const urlsToSkip = [
'/favicon.ico'
]
let bundle: object = null
let clientManifest: object = null
let renderer = null
let template: string = null
export default async function setupDevServer (app): Promise<void> {
const promise = new DeferredPromise()
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, async (err: any, stats: any) => {
logWebpack(err, stats)
const bundlePath = path.join(serverConfig.output.path, serverBundleFileName)
bundle = JSON.parse(await mfs.readFileAsync(bundlePath, 'utf-8'))
updateRenderer(promise.resolve)
})
await Promise.all([setupClientDevMiddlware(app, promise.resolve), setupTemplate()])
app.use(handler)
// @ts-ignore
return promise
}
async function setupClientDevMiddlware (app, resolveSetup) {
const clientDevMiddleware = await koaWebpack({
compiler: clientCompiler
})
clientCompiler.hooks.done.tap('updateRender', async (stats: any) => {
logWebpack(undefined, stats)
const manifestPath = path.join(clientConfig.output.path, clientManifestFileName)
const fileSystem = clientDevMiddleware.devMiddleware.fileSystem
clientManifest = JSON.parse(await fileSystem.readFileAsync(manifestPath, 'utf-8'))
updateRenderer(resolveSetup)
})
app.use(clientDevMiddleware)
}
async function setupTemplate () {
const templatePath = path.resolve(__dirname, '../index.html')
template = await fsAsync.readFile(templatePath, 'utf-8') as string
}
function logWebpack (err: Error, stats) {
if (err) {
throw err
}
stats = stats.toJson()
stats.errors.forEach((err: any) => console.log(err))
stats.warnings.forEach((warn: any) => console.log(warn))
}
async function updateRenderer (resolveSetup: () => void) {
if (!bundle || !clientManifest || !template) {
return
}
renderer = createBundleRenderer(bundle, {
clientManifest,
template,
runInNewContext: false
})
if (resolveSetup) {
resolveSetup()
}
}
async function handler (ctx) {
const { url } = ctx
// TODO add favicon skip favicon.
if (urlsToSkip.includes(url)) {
ctx.body = ''
return
}
if (!bundle || !clientManifest || !renderer) {
ctx.body = 'Please wait...'
return
}
try {
ctx.response.header['Content-Type'] = 'text/html'
ctx.body = await renderer.renderToString({ url })
} catch (error) {
console.error('[Error] SSR render error:', error)
ctx.body = 500
ctx.body = error.message || 'Unknown Internal Server Error'
}
}

View File

@ -0,0 +1,36 @@
// TODO move imports here and make no dynamic
import path from 'path'
import fs from 'fs'
import { createBundleRenderer } from 'vue-server-renderer'
// TODO move prefix to config
import bundle from '../src/vue-ssr-server-bundle.json'
import clientManifest from '../src/vue-ssr-client-manifest.json'
import serve from 'koa-static'
import koaMount from 'koa-mount'
const fsAsync = fs.promises
export default async function setupProdServer (app) {
const template = await fsAsync.readFile(path.resolve(__dirname, '../src/index.html'), 'utf-8') as string
const renderer = createBundleRenderer(bundle, {
template,
clientManifest,
runInNewContext: false
})
async function handler (ctx) {
const context = {
url: ctx.path
}
try {
ctx.body = await renderer.renderToString(context)
} catch (error) {
ctx.body = error
}
}
app.use(koaMount('/src', serve(path.resolve(__dirname, '../src/'))))
app.use(handler)
}

View File

@ -1,30 +0,0 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const cssLoader = {
test: /\.css$/,
use: [
'vue-style-loader',
{ loader: 'css-loader', options: { sourceMap: false, importLoaders: 1 } },
'postcss-loader'
]
}
const stylusLoader = {
test: /\.(styl|stylus)$/,
use: [
'vue-style-loader',
{ loader: 'css-loader', options: { sourceMap: false, importLoaders: 1 } },
'postcss-loader',
{ loader: 'stylus-loader', options: { sourceMap: false } }
]
}
if (process.env.NODE_ENV === 'production') {
cssLoader.use = [MiniCssExtractPlugin.loader].concat(cssLoader.use)
stylusLoader.use = [MiniCssExtractPlugin.loader].concat(stylusLoader.use)
}
module.exports = {
cssLoader,
stylusLoader
}

View File

@ -1,68 +1,156 @@
const webpack = require('webpack')
const path = require('path') const path = require('path')
const { DefinePlugin } = require('webpack')
const { VueLoaderPlugin } = require('vue-loader') const { VueLoaderPlugin } = require('vue-loader')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const TSLintPlugin = require('tslint-webpack-plugin')
const { cssLoader, stylusLoader } = require('./style-loader.conf')
const { clientPort, ssrPort } = require('./build-config') const isProduction = process.env.NODE_ENV === 'production'
const isDev = process.env.NODE_ENV === 'development'
module.exports = {
mode: isDev ? 'development' : 'production',
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? false : 'cheap-module-source-map',
output: { output: {
publicPath: isDev path: path.resolve(__dirname, '../dist/src'),
? `//localhost:${clientPort}/` // Please bind this hostname to 127.0.0.1 when developing. publicPath: '/src/',
: '/' // filename: '[name].[hash].js'
filename: '[name].js'
}, },
optimization: {},
resolve: { resolve: {
extensions: ['.js', '.ts'], extensions: [
'.mjs',
'.js',
'.jsx',
'.ts',
'.tsx'
],
alias: { alias: {
client: path.resolve(__dirname, '../client/') client: path.resolve(__dirname, '../client/'),
} utils: path.resolve(__dirname, '../utils/')
},
modules: ['node_modules']
},
stats: {
modules: false
}, },
module: { module: {
rules: [ rules: [
cssLoader, {
stylusLoader, test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
}
]
},
{ {
test: /\.js$/, test: /\.js$/,
exclude: /node_modules/,
use: { use: {
loader: 'babel-loader' loader: 'babel-loader'
} }
}, },
{ {
test: /\.tsx?$/, test: /\.ts$/,
use: [ use: [
'babel-loader', {
loader: 'babel-loader'
},
{ {
loader: 'ts-loader', loader: 'ts-loader',
options: { options: {
happyPackMode: true, transpileOnly: true,
appendTsSuffixTo: [/\.vue$/] appendTsSuffixTo: [/\.vue$/]
} }
} }
], ]
exclude: /node_modules/
}, },
{ {
test: /\.vue$/, test: /\.tsx$/,
loader: 'vue-loader' use: [
{
loader: 'babel-loader'
},
{
loader: 'ts-loader',
options: {
transpileOnly: true,
appendTsxSuffixTo: [/\.vue$/]
}
}
]
}, },
{ {
test: /\.(png|jpg|gif|svg|woff|woff2|eot|ttf)$/, test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [ use: [
{ {
loader: 'url-loader', loader: 'url-loader',
options: { options: {
limit: 8192, limit: 4096,
name: 'img/[name].[hash:7].[ext]' fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.css$/,
use: [
{
loader: 'vue-style-loader',
options: {
sourceMap: false,
shadowMode: false
}
},
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: false
}
}
]
},
{
test: /\.scss$/,
use: [
{
loader: 'vue-style-loader',
options: {
sourceMap: false,
shadowMode: false
}
},
{
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2
}
},
{
loader: 'postcss-loader',
options: {
sourceMap: false
}
},
{
loader: 'sass-loader',
options: {
sourceMap: false
} }
} }
] ]
@ -71,23 +159,12 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.ContextReplacementPlugin(
/moment[\/\\]locale$/,
/ru-RU/
),
new webpack.DefinePlugin({
BASE_URL: JSON.stringify(`http://localhost:${ssrPort}`)
}),
new FriendlyErrorsPlugin(),
new VueLoaderPlugin(), new VueLoaderPlugin(),
new FriendlyErrorsWebpackPlugin(),
new TSLintPlugin({ new DefinePlugin({
files: [ NODE_ENV: isProduction ? "'production'" : "'development'"
'./client/**/*.ts',
'./server/**/*.ts',
'./client/**/*.tsx',
'./server/**/*.tsx'
]
}) })
] ]
} }
module.exports = config

View File

@ -1,69 +1,39 @@
const path = require('path') const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const merge = require('webpack-merge') const merge = require('webpack-merge')
const VueClientPlugin = require('vue-server-renderer/client-plugin') const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const WebpackBar = require('webpackbar')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const { appName, clientPort, distPath, env } = require('./build-config')
const baseConfig = require('./webpack.base.conf') const baseConfig = require('./webpack.base.conf')
const isProduction = process.env.NODE_ENV === 'production' const isProduction = process.env.NODE_ENV === 'production'
const webpackConfig = merge(baseConfig, { const webpackConfig = merge(baseConfig, {
entry: { // Entry should be array cause of webpack-hot-client requirements
app: rootResolve('./client/entry.client.ts') mode: isProduction ? 'production' : 'development',
}, entry: [path.resolve(__dirname, '../client/entry.client.ts')],
devtool: isProduction ? false : 'cheap-module-source-map',
output: { optimization: {
path: distPath, splitChunks: {
filename: `static/js/[name].${isProduction ? '[hash].' : ''}js` name: 'manifest',
minChunks: Infinity
}
}, },
plugins: [ plugins: [
new VueClientPlugin(), new VueSSRClientPlugin(),
new WebpackBar({
new webpack.DefinePlugin(Object.assign({}, env, { name: 'Client'
'process.env.VUE_ENV': JSON.stringify('client')
})),
new HtmlWebpackPlugin({
template: rootResolve('./index.html'),
minify: true,
inject: true,
title: appName
}) })
// TODO make it work
/* ,
new ForkTsCheckerWebpackPlugin({
vue: true,
tslint: true,
formatter: 'codeframe',
checkSyntacticErrors: false
}) */
] ]
}) })
if (isProduction) {
webpackConfig.plugins.push(
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash].css',
chunkFilename: 'static/css/[id].[contenthash].css'
})
)
webpackConfig.optimization.runtimeChunk = true
} else {
webpackConfig.devtool = 'eval-source-map'
webpackConfig.plugins.push(
new webpack.HotModuleReplacementPlugin()
)
webpackConfig.devServer = {
headers: {
'Access-Control-Allow-Origin': '*'
},
hot: true,
quiet: true,
compress: false,
port: clientPort,
proxy: {},
historyApiFallback: true
}
}
module.exports = webpackConfig module.exports = webpackConfig
function rootResolve (filePath) {
return path.resolve(__dirname, '../', filePath)
}

View File

@ -1,60 +1,37 @@
const path = require('path') const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge') const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals') const nodeExternals = require('webpack-node-externals')
const MiniCssExtractPlugin = require('mini-css-extract-plugin') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueServerPlugin = require('vue-server-renderer/server-plugin') const WebpackBar = require('webpackbar')
const axios = require('axios')
const { distPath, env, ssrPort } = require('./build-config')
const baseConfig = require('./webpack.base.conf') const baseConfig = require('./webpack.base.conf')
axios.defaults.baseURL = `http://localhost:${ssrPort}` const isProduction = process.env.NODE_ENV === 'production'
const webpackConfig = merge(baseConfig, { const webpackConfig = merge(baseConfig, {
mode: process.env.NODE_ENV, mode: isProduction ? 'production' : 'development',
target: 'node', target: 'node',
devtool: isProduction ? false : 'source-map',
entry: rootResolve('./client/entry.server.ts'), entry: path.resolve(__dirname, '../client/entry.server.ts'),
output: { output: {
path: distPath, filename: 'server-bundle.js',
filename: 'server-build.js',
libraryTarget: 'commonjs2' libraryTarget: 'commonjs2'
}, },
externals: nodeExternals({ externals: nodeExternals({
whitelist: [ whitelist: [
/\.css$/, /\.css$/,
/\.vue$/, /\.sass$/,
/babel-polyfill/ /\.scss$/,
/\.svg$/
] ]
}), }),
plugins: [ plugins: [
new VueServerPlugin(), new VueSSRServerPlugin(),
new WebpackBar({
new webpack.DefinePlugin(Object.assign({}, env, { name: 'Server',
'process.env.VUE_ENV': JSON.stringify('server') color: 'orange'
})) })
] ]
}) })
switch (process.env.NODE_ENV) {
case 'production':
webpackConfig.plugins.push(
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash].css',
chunkFilename: 'static/css/[id].[contenthash].css'
})
)
webpackConfig.optimization.splitChunks = false
break
}
module.exports = webpackConfig module.exports = webpackConfig
function rootResolve (filePath) {
return path.resolve(__dirname, '../', filePath)
}

View File

@ -17,10 +17,9 @@ import 'client/assets/code-highlight.css'
import AppComponent from './App.vue' import AppComponent from './App.vue'
import { createRouter } from './router' import { createRouter } from './router'
import { createStore } from './store' import { createStore } from './store'
import config from '../config'
// webpack DefinePlugin constant, see build/webpack.base.conf.js for axios.defaults.baseURL = `http://localhost:${config.port}`
declare var BASE_URL: string
axios.defaults.baseURL = BASE_URL
const icons = {} const icons = {}
// TODO import and add only used icons for production // TODO import and add only used icons for production

View File

@ -1,5 +1,3 @@
import 'reflect-metadata'
import '@babel/polyfill'
import _get from 'lodash/get' import _get from 'lodash/get'
import { createApp } from './app' import { createApp } from './app'

View File

@ -1,4 +1,3 @@
import 'reflect-metadata'
import _get from 'lodash/get' import _get from 'lodash/get'
import { createApp } from './app' import { createApp } from './app'
@ -10,6 +9,7 @@ export default async context => {
router.onReady(() => { router.onReady(() => {
const matchedComponents = router.getMatchedComponents() const matchedComponents = router.getMatchedComponents()
// TODO not reject, create fallback to 404 component
if (!matchedComponents.length) { if (!matchedComponents.length) {
return reject({ return reject({
code: 404, code: 404,

View File

@ -1,14 +0,0 @@
export default class DeferredPromise {
constructor () {
this._promise = new Promise((resolve, reject) => {
// assign the resolve and reject functions to `this`
// making them usable on the class instance
this.resolve = resolve
this.reject = reject
})
// bind `then` and `catch` to implement the same interface as Promise
this.then = this._promise.then.bind(this._promise)
this.catch = this._promise.catch.bind(this._promise)
this[Symbol.toStringTag] = 'Promise'
}
}

View File

@ -1,7 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import { Mixin } from 'vue-mixin-decorator' import { Mixin } from 'vue-mixin-decorator'
import ConfirmDialog from 'client/components/ConfirmDialog.vue' import ConfirmDialog from 'client/components/ConfirmDialog.vue'
import DeferredPromise from 'client/helpers/DeferredPromise' import DeferredPromise from 'utils/DeferredPromise'
const ComponentClass = Vue.extend(ConfirmDialog) const ComponentClass = Vue.extend(ConfirmDialog)

View File

@ -1,7 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import { Mixin } from 'vue-mixin-decorator' import { Mixin } from 'vue-mixin-decorator'
import ConflictDialog from 'client/components/ConflictDialog.vue' import ConflictDialog from 'client/components/ConflictDialog.vue'
import DeferredPromise from 'client/helpers/DeferredPromise' import DeferredPromise from 'utils/DeferredPromise'
const ComponentClass = Vue.extend(ConflictDialog) const ComponentClass = Vue.extend(ConflictDialog)

View File

@ -6,10 +6,10 @@ interface ICategoryState {
category: ICategoryFull category: ICategoryFull
} }
const state: ICategoryState = { const state = (): ICategoryState => ({
categoryList: [], categoryList: [],
category: null category: null
} })
const getters: GetterTree<ICategoryState, any> = {} const getters: GetterTree<ICategoryState, any> = {}

View File

@ -11,9 +11,9 @@ interface ICategoryItemState {
categoryItemList: ICategoryItem[] categoryItemList: ICategoryItem[]
} }
const state: ICategoryItemState = { const state = (): ICategoryItemState => ({
categoryItemList: [] categoryItemList: []
} })
const getters: GetterTree<ICategoryItemState, any> = {} const getters: GetterTree<ICategoryItemState, any> = {}
@ -118,7 +118,7 @@ const actions: ActionTree<ICategoryItemState, any> = {
}, },
async updateCategoryDescription ( async updateCategoryDescription (
{ dispatch }: ActionContext<ICategoryItemState, any>, { dispatch }: ActionContext<ICategoryItemState, any>,
{ id, original, modified }: {id: string, original: string, modified: string} { id, original, modified }: { id: string, original: string, modified: string }
) { ) {
const createdDescription = await CategoryItemService.updateCategoryDescription({ const createdDescription = await CategoryItemService.updateCategoryDescription({
id, id,

View File

@ -6,10 +6,10 @@ interface IWikiState {
searchInput: string searchInput: string
} }
const state: IWikiState = { const state = (): IWikiState => ({
searchResults: [], searchResults: [],
searchInput: '' searchInput: ''
} })
const getters: GetterTree<IWikiState, any> = {} const getters: GetterTree<IWikiState, any> = {}

4
front/config.js Normal file
View File

@ -0,0 +1,4 @@
export default {
apiUrl: 'https://staging.guide.aelve.com:4400/',
port: process.env.PORT || 5000
}

View File

@ -1,3 +0,0 @@
{
"apiUrl": "http://localhost:4400"
}

4
front/index.d.ts vendored
View File

@ -1,4 +0,0 @@
declare module '*.vue' {
const content: any
export default content
}

View File

@ -3,10 +3,12 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %></title> <title> Aelve Guide </title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app">
<!--vue-ssr-outlet-->
</div>
</body> </body>
</html> </html>

5870
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,84 +4,82 @@
"description": "Aelve Guide", "description": "Aelve Guide",
"private": true, "private": true,
"scripts": { "scripts": {
"client:dev": "cross-env NODE_ENV=development webpack-dev-server --config ./build/webpack.client.conf.js", "dev": "cross-env NODE_ENV=development nodemon --watch",
"client:build": "cross-env NODE_ENV=production webpack --config ./build/webpack.client.conf.js", "build": "cross-env NODE_ENV=production rimraf dist && npm run build:src && copyfiles index.html package.json dist/src && copyfiles package.json dist && tsc -p ./prod.tsconfig.json --outDir dist",
"server:dev": "cross-env NODE_ENV=development nodemon ./server/bin.js --watch server/**/*.js", "build:src": "concurrently \"npm run build:client\" \"npm run build:server\"",
"server:build": "cross-env NODE_ENV=production webpack --config ./build/webpack.server.conf.js", "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.conf.js --hide-modules",
"build-all": "npm run client:build && npm run server:build", "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --hide-modules",
"start:dev": "concurrently \"npm run client:dev\" \"npm run server:dev\"",
"start:prod": "cross-env NODE_ENV=production node server/bin.js",
"test": "testcafe chrome client/tests/*.test.js --app \"npm run start:dev\" --app-init-delay 10000" "test": "testcafe chrome client/tests/*.test.js --app \"npm run start:dev\" --app-init-delay 10000"
}, },
"nodemonConfig": {
"ext": "ts",
"exec": "ts-node -O {\\\"module\\\":\\\"commonjs\\\"} ./server.ts",
"watch": [
"build/**/*.js",
"build/**/*.ts",
"config.js"
],
"ignore": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
},
"dependencies": { "dependencies": {
"@junyiz/koa-proxy-pass": "^1.2.1", "@fortawesome/fontawesome-svg-core": "^1.2.17",
"@fortawesome/free-solid-svg-icons": "^5.8.1",
"@fortawesome/vue-fontawesome": "^0.1.6",
"axios": "^0.18.0", "axios": "^0.18.0",
"easymde": "^2.4.2", "easymde": "^2.5.1",
"koa": "^2.5.0", "koa": "^2.7.0",
"koa-bodyparser": "^4.2.0", "koa-bodyparser": "^4.2.1",
"koa-proxies": "^0.7.0", "koa-mount": "^4.0.0",
"koa-proxy": "^0.9.0", "koa-proxy": "^0.9.0",
"koa-router": "^7.4.0", "koa-static": "^5.0.0",
"koa-static": "^4.0.2", "lodash": "^4.17.11",
"koa-static-server": "^1.3.4", "moment": "^2.24.0",
"lodash": "^4.17.10", "normalize-url": "^4.3.0",
"moment": "^2.22.1",
"normalize-url": "^4.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"reflect-metadata": "^0.1.12", "vue": "^2.6.10",
"vue": "^2.6.6", "vue-class-component": "^7.0.2",
"vue-class-component": "^7.0.1", "vue-mixin-decorator": "^1.1.1",
"vue-mixin-decorator": "^1.0.0", "vue-property-decorator": "^8.1.0",
"vue-property-decorator": "^7.0.0", "vue-router": "^3.0.4",
"vue-router": "^3.0.1", "vue-server-renderer": "^2.6.10",
"vue-server-renderer": "^2.6.6", "vuetify": "^1.5.11",
"vuetify": "^1.5.5", "vuex": "^3.1.0",
"vuex": "^3.0.1",
"vuex-router-sync": "^5.0.0" "vuex-router-sync": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.2.2", "@babel/core": "^7.4.3",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@vue/preload-webpack-plugin": "^1.1.0",
"@babel/plugin-transform-runtime": "^7.2.0", "autoprefixer": "^9.5.1",
"@babel/polyfill": "^7.2.5",
"@babel/preset-env": "^7.3.1",
"@fortawesome/fontawesome-free": "^5.3.1",
"@fortawesome/fontawesome-svg-core": "^1.2.12",
"@fortawesome/free-solid-svg-icons": "^5.6.3",
"@fortawesome/vue-fontawesome": "^0.1.4",
"@types/lodash": "^4.14.116",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"babel-plugin-transform-runtime": "^6.23.0", "concurrently": "^4.1.0",
"babel-preset-env": "^1.7.0", "copyfiles": "^2.1.0",
"babel-preset-stage-2": "^6.24.1",
"concurrently": "^3.5.1",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^0.28.11", "css-loader": "^2.1.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^3.0.1",
"file-loader": "^1.1.11", "fork-ts-checker-webpack-plugin": "^1.0.2",
"friendly-errors-webpack-plugin": "^1.7.0", "friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^3.2.0", "koa-webpack": "^5.2.2",
"material-design-icons-iconfont": "^3.0.3",
"memory-fs": "^0.4.1", "memory-fs": "^0.4.1",
"mini-css-extract-plugin": "^0.4.0", "nodemon": "^1.18.11",
"nodemon": "^1.17.5", "postcss-loader": "^3.0.0",
"postcss-loader": "^2.1.5", "rimraf": "^2.6.3",
"stylus": "^0.54.5", "testcafe": "^1.1.2",
"stylus-loader": "^3.0.2", "testcafe-vue-selectors": "^3.1.0",
"testcafe": "^1.0.1", "ts-loader": "^5.3.3",
"testcafe-vue-selectors": "^3.0.0", "ts-node": "^8.0.3",
"ts-loader": "^4.4.2", "tslint": "^5.15.0",
"tslint": "^5.11.0", "typescript": "^3.4.3",
"tslint-webpack-plugin": "^1.2.2", "url-loader": "^1.1.2",
"typescript": "^2.9.2", "vue-loader": "^15.7.0",
"url-loader": "^1.0.1",
"vue-loader": "^15.6.2",
"vue-style-loader": "^4.1.2", "vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.6", "vue-template-compiler": "^2.6.10",
"webpack": "^4.29.3", "webpack": "^4.30.0",
"webpack-cli": "^3.2.1", "webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.1.14", "webpack-merge": "^4.2.1",
"webpack-merge": "^4.1.2", "webpack-node-externals": "^1.7.2",
"webpack-node-externals": "^1.7.2" "webpackbar": "^3.1.5"
} }
} }

View File

@ -1,3 +1,5 @@
// TODO configure
const autoprefixer = require('autoprefixer') const autoprefixer = require('autoprefixer')
const plugins = [ const plugins = [

27
front/prod.tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"files": [
"./server.ts"
],
"compilerOptions": {
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"module": "commonjs",
"target": "esnext",
"strict": false,
"esModuleInterop": true,
"resolveJsonModule": true,
"sourceMap": false,
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"strictNullChecks": false,
"lib": [
"dom",
"es2017",
"es5"
]
}
}

34
front/server.ts Normal file
View File

@ -0,0 +1,34 @@
import Koa from 'koa'
import bodyparser from 'koa-bodyparser'
import proxy from 'koa-proxy'
import config from './config.js'
const { port, apiUrl } = config
const isProduction = process.env.NODE_ENV === 'production'
async function start () {
const app = new Koa()
// TODO replace proxy lib or write own middleware for log and flexibility
app.use(proxy({
requestOptions: {
strictSSL: false
},
host: apiUrl,
match: /^\/api\//,
map: (path: string) => path.replace('/api', '')
}))
app.use(bodyparser())
const setupServer = isProduction
? (await import('./build/setupProdServer')).default
: (await import('./build/setupDevServer')).default
await setupServer(app)
app.listen(port, () => {
console.log(`[Info] Server is on at ${port}.`)
})
}
start()

View File

@ -1,38 +0,0 @@
const path = require('path')
// TODO заменить на express
const Koa = require('koa')
const serve = require('koa-static')
const bodyparser = require('koa-bodyparser')
const { ssrPort } = require('../build/build-config')
const proxy = require('koa-proxy')
const config = require('../config.json')
const app = new Koa()
app.use(proxy({
host: config.apiUrl,
match: /^\/api\//,
map: function (path) {
return path.replace('/api', '')
}
}))
app.use(bodyparser())
let ssrRouter = null
switch (process.env.NODE_ENV) {
case 'production':
app.use(serve(path.resolve(__dirname, '../dist'), {
index: 'does-not-exist.html' // Set to anything but 'index.html' to use ssr.
}))
ssrRouter = require('./ssr.prod.js')
break
case 'development':
ssrRouter = require('./ssr.dev.js')
break
}
app.use(ssrRouter)
app.listen(ssrPort, () => {
console.log(`[Info] Server is on at ${ssrPort}.`)
})

View File

@ -1,14 +0,0 @@
const { appName } = require('../build/build-config')
const moment = require('moment')
function templateEnvs () {
return {
title: appName,
isSSR: true,
renderTime: moment(new Date()).format('YYYY-MM-DD HH:mm:ss')
}
}
module.exports = {
templateEnvs
}

View File

@ -1,75 +0,0 @@
const axios = require('axios')
const fs = require('fs')
const path = require('path')
const MemoryFs = require('memory-fs')
const webpack = require('webpack')
const VueServerRenderer = require('vue-server-renderer')
const serverConfig = require('../build/webpack.server.conf')
const serverCompiler = webpack(serverConfig)
const { clientPort } = require('../build/build-config')
const { templateEnvs } = require('./ssr.config')
const mfs = new MemoryFs()
serverCompiler.outputFileSystem = mfs
let bundle = null
serverCompiler.watch({}, (err, stats) => {
if (err) {
throw err
}
stats = stats.toJson()
stats.errors.forEach(err => console.log(err))
stats.warnings.forEach(warn => console.log(warn))
const bundlePath = path.join(serverConfig.output.path, 'vue-ssr-server-bundle.json')
bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'))
})
const urlsToSkip = [
'/favicon.ico'
]
module.exports = async function handler (ctx) {
const url = ctx.url
// Skip favicon.
if (urlsToSkip.indexOf(url) > -1) {
ctx.body = ''
return
}
if (!bundle) {
ctx.body = 'Please wait...'
return
}
const clientManifestUrl = `http://${ctx.hostname}:${clientPort}/vue-ssr-client-manifest.json`
let clientManifest = null
try {
const { data } = await axios.get(clientManifestUrl)
clientManifest = data
} catch (error) {
console.error(`[Error] Failed to get ${clientManifestUrl}:`, error)
ctx.body = error.message || 'Failed to get client manifest json'
return
}
const renderer = VueServerRenderer.createBundleRenderer(bundle, {
clientManifest,
template: fs.readFileSync(path.resolve(__dirname, './template.html'), 'utf-8'),
runInNewContext: false
})
const context = Object.assign(templateEnvs(), {
url
})
try {
ctx.body = await renderer.renderToString(context)
} catch (error) {
console.error('[Error] SSR render error:', error)
ctx.body = error.message || 'SSR unknown renderer error'
}
}

View File

@ -1,23 +0,0 @@
const path = require('path')
const fs = require('fs')
const { createBundleRenderer } = require('vue-server-renderer')
const { templateEnvs } = require('./ssr.config')
const bundle = require('../dist/vue-ssr-server-bundle.json')
const renderer = createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, './template.html'), 'utf-8'),
clientManifest: require('../dist/vue-ssr-client-manifest.json'),
runInNewContext: false
})
module.exports = async function handler (ctx) {
const context = Object.assign(templateEnvs(), {
url: ctx.path
})
try {
ctx.body = await renderer.renderToString(context)
} catch (error) {
ctx.body = error
}
}

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
</head>
<body>
<div id="app">
<!--vue-ssr-outlet-->
</div>
</body>
</html>

View File

@ -13,15 +13,17 @@
"moduleResolution": "node", "moduleResolution": "node",
"module": "esnext", "module": "esnext",
"target": "es5", "target": "es5",
"strict": true, "strict": false,
"esModuleInterop": true,
"resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"declaration": true,
"skipLibCheck": true, "skipLibCheck": true,
"skipDefaultLibCheck": true, "skipDefaultLibCheck": true,
"strictNullChecks": false, "strictNullChecks": false,
"lib": [ "lib": [
"dom", "dom",
"es2017" "es2017",
] "es5"
],
} }
} }

View File

@ -7,7 +7,7 @@
"rules": { "rules": {
"arrow-parens": false, "arrow-parens": false,
"comment-format": [true, "check-space"], "comment-format": [true, "check-space"],
"max-classes-per-file": [false], "max-classes-per-file": false,
"member-access": false, "member-access": false,
"member-ordering": false, "member-ordering": false,
"no-console": false, "no-console": false,

11
front/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
declare module '*.vue' {
const content: any
export default content
}
declare module '*/config.js' {
const content: any
export default content
}
declare module 'koa-proxy'

View File

@ -0,0 +1,137 @@
// export default class DeferredPromise<T = any> extends Promise<T> {
// readonly [Symbol.toStringTag]: 'Promise'
// // public resolve: (value?: T | PromiseLike<T>) => void
// // public reject: (reason?: any) => void
// constructor () {
// super((resolve, reject) => {
// this.resolve = resolve
// this.reject = reject
// })
// }
// }
// TODO refactor
export default class DeferredPromise {
resolve: any
reject: any
_promise: any
catch: any
then: any
constructor () {
this._promise = new Promise((resolve, reject) => {
// assign the resolve and reject functions to `this`
// making them usable on the class instance
this.resolve = resolve
this.reject = reject
})
// bind `then` and `catch` to implement the same interface as Promise
this.then = this._promise.then.bind(this._promise)
this.catch = this._promise.catch.bind(this._promise)
this[Symbol.toStringTag] = 'Promise'
}
}
// export type PromiseStatus = 'resolved' | 'rejected' | 'pending'
// export default class DeferredPromise<T = any> {
// static RESOLVED: string = 'resolved'
// static REJECTED: string = 'rejected'
// static PENDING: string = 'pending'
// static resolve<T> (value?: T): DeferredPromise<T> {
// const promise = new DeferredPromise<T>()
// promise.resolve(value)
// return promise
// }
// static reject<T> (reason: any): DeferredPromise<T> {
// const promise = new DeferredPromise<T>()
// promise.reject(reason)
// return promise
// }
// private _promise: Promise<T>
// private _resolve: ((value: T) => any)
// private _reject: ((error: any) => any)
// private _status: PromiseStatus
// constructor (callback?: (deferred: DeferredPromise<T>) => any) {
// this._status = DeferredPromise.PENDING as PromiseStatus
// this._promise = new Promise<T>((resolve: any, reject: any) => {
// this._resolve = resolve
// this._reject = reject
// if (callback !== void 0) { callback.call(this, this) }
// }).then((value: T) => {
// this._status = DeferredPromise.RESOLVED as PromiseStatus
// return value
// }, (error: any) => {
// this._status = DeferredPromise.REJECTED as PromiseStatus
// throw error
// })
// }
// get status (): PromiseStatus {
// return this._status
// }
// get promise (): Promise<T> {
// return this._promise
// }
// resolve (value?: T): void {
// if (this._status === DeferredPromise.PENDING) {
// this._resolve(value)
// } else {
// throw new TypeError('promise already resolved/rejected')
// }
// }
// reject (reason: any): void {
// if (this._status === DeferredPromise.PENDING) {
// this._reject(reason)
// } else {
// throw new TypeError('promise already resolved/rejected')
// }
// }
// then<A = any> (onFulfilled: ((result: T) => A), onRejected?: ((reason: any) => A)): DeferredPromise<A> {
// return new DeferredPromise<A>((deferred: DeferredPromise<A>) => {
// this._promise.then((result: T) => {
// try {
// deferred.resolve(onFulfilled(result))
// } catch (error) {
// deferred.reject(error)
// }
// }, (reason: any) => {
// if (onRejected === void 0) {
// deferred.reject(reason)
// } else {
// try {
// deferred.resolve(onRejected(reason))
// } catch (error) {
// deferred.reject(error)
// }
// }
// })
// })
// }
// catch<A = any> (onRejected: ((reason: any) => A)): DeferredPromise<A | T> {
// return new DeferredPromise<A | T>((deferred: DeferredPromise<A | T>) => {
// this._promise.then((result: T) => {
// deferred.resolve(result)
// }, (reason: any) => {
// try {
// deferred.resolve(onRejected(reason))
// } catch (error) {
// deferred.reject(error)
// }
// })
// })
// }
// }