diff --git a/.autod.conf.js b/.autod.conf.js
new file mode 100644
index 0000000..caf2edc
--- /dev/null
+++ b/.autod.conf.js
@@ -0,0 +1,27 @@
+'use strict';
+
+module.exports = {
+ write: true,
+ prefix: '^',
+ plugin: 'autod-egg',
+ test: [
+ 'test',
+ 'benchmark',
+ ],
+ dep: [
+ 'egg'
+ ],
+ devdep: [
+ 'egg-ci',
+ 'egg-bin',
+ 'autod',
+ 'eslint',
+ 'eslint-config-egg',
+ 'webstorm-disable-index',
+ ],
+ exclude: [
+ './test/fixtures',
+ './dist',
+ ],
+};
+
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..c799fe5
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": "eslint-config-egg"
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6a71ac1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+logs/
+npm-debug.log
+node_modules/
+coverage/
+.idea/
+run/
+.DS_Store
+*.swp
+package-lock.json
+yarn.lock
+.nyc_output/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ce21122
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+sudo: false
+language: node_js
+node_js:
+ - '8'
+ - '10'
+install:
+ - npm i npminstall && npminstall
+script:
+ - npm run ci
+after_script:
+ - npminstall codecov && codecov
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..139597f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,2 @@
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cde8f04
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# res
+
+[![NPM version][npm-image]][npm-url]
+[![build status][travis-image]][travis-url]
+[![Test coverage][codecov-image]][codecov-url]
+[![David deps][david-image]][david-url]
+[![Known Vulnerabilities][snyk-image]][snyk-url]
+[![npm download][download-image]][download-url]
+
+[npm-image]: https://img.shields.io/npm/v/res.svg?style=flat-square
+[npm-url]: https://npmjs.org/package/res
+[travis-image]: https://img.shields.io/travis/easy-team/res.svg?style=flat-square
+[travis-url]: https://travis-ci.org/easy-team/res
+[codecov-image]: https://img.shields.io/codecov/c/github/easy-team/res.svg?style=flat-square
+[codecov-url]: https://codecov.io/github/easy-team/res?branch=master
+[david-image]: https://img.shields.io/david/easy-team/res.svg?style=flat-square
+[david-url]: https://david-dm.org/easy-team/res
+[snyk-image]: https://snyk.io/test/npm/res/badge.svg?style=flat-square
+[snyk-url]: https://snyk.io/test/npm/res
+[download-image]: https://img.shields.io/npm/dm/res.svg?style=flat-square
+[download-url]: https://npmjs.org/package/res
+
+
+A Powerful, Simple React Node Web Framework, Front-End and Node of The Application are Written in TypeScript or JavaScript! Everything is Simple!
+
+> You don't need to care about React server-side rendering implementation details and the Webpack + Babel + TypeScript build process. You just need to write React isomorphic applications just like you would write a React front-end application.
+
+## Installation
+
+```bash
+$ npm install @easy-team/res --save
+```
+
+Node.js >= 8.0.0 required.
+
+## Features
+
+- ✔︎ Based on [Egg](https://eggjs.org/en/intro/index.html) Framework, Powerful, Easy to Expand
+- ✔︎ Support React Server Side Render and Client Side Render Modes, Nunjucks & React Render Mode, Rendering Cache, Automatic Downgrade,
+- ✔︎ Front-End and Node of The Application are Written in TypeScript or JavaScript
+- ✔︎ Build with Webpack, Auto Building, Hot Reload, Code Splitting, High Speed, Performance Optimization
+- ✔︎ Powerful Tool Chain [res-cli](https://github.com/easy-team/res-cli)
+
+## Document
+
+- https://www.yuque.com/easy-team/res
+
+## QuickStart
+
+```bash
+$ npm install -g @easy-team/res-cli
+$ res init
+$ npm install
+$ npm run dev
+$ open http://localhost:7001
+```
+
+## Examples
+
+See [res-awesome](https://github.com/easy-team/res-awesome)
+
+## Links
+
+- https://eggjs.org/en/advanced/framework.html
+- https://www.yuque.com/easy-team/res
+
+## License
+
+[MIT](LICENSE)
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..f701fbc
--- /dev/null
+++ b/app.js
@@ -0,0 +1,5 @@
+'use strict';
+// https://github.com/eggjs/egg/issues/1520
+module.exports = app => {
+ app.config.coreMiddleware.push('locals');
+};
diff --git a/app/extend/application.js b/app/extend/application.js
new file mode 100644
index 0000000..0e903a8
--- /dev/null
+++ b/app/extend/application.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+
+};
diff --git a/app/extend/context.js b/app/extend/context.js
new file mode 100644
index 0000000..0e903a8
--- /dev/null
+++ b/app/extend/context.js
@@ -0,0 +1,5 @@
+'use strict';
+
+module.exports = {
+
+};
diff --git a/app/middleware/locals.js b/app/middleware/locals.js
new file mode 100644
index 0000000..d7303b5
--- /dev/null
+++ b/app/middleware/locals.js
@@ -0,0 +1,7 @@
+'use strict';
+module.exports = () => {
+ return async function(ctx, next) {
+ ctx.locals.title = 'res';
+ await next();
+ };
+};
diff --git a/app/service/test.js b/app/service/test.js
new file mode 100644
index 0000000..26a61fc
--- /dev/null
+++ b/app/service/test.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const Service = require('egg').Service;
+
+class TestService extends Service {
+ constructor(ctx) {
+ super(ctx);
+ this.config = this.app.config.test;
+ }
+
+ async get(id) {
+ return { id, name: this.config.key };
+ }
+}
+
+module.exports = TestService;
diff --git a/app/web/tsconfig.json b/app/web/tsconfig.json
new file mode 100644
index 0000000..ec03a65
--- /dev/null
+++ b/app/web/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../config/tsconfig.json",
+ "compilerOptions": {
+ "target": "es5",
+ "module": "esnext",
+ "lib": [
+ "es6",
+ "dom"
+ ]
+ },
+
+ "exclude": [
+ "node_modules",
+ "**/*.spec.ts"
+ ]
+}
\ No newline at end of file
diff --git a/app/web/typings/vue.d.ts b/app/web/typings/vue.d.ts
new file mode 100644
index 0000000..dc48246
--- /dev/null
+++ b/app/web/typings/vue.d.ts
@@ -0,0 +1,4 @@
+declare module "*.vue" {
+ import Vue from 'vue'
+ export default Vue
+}
\ No newline at end of file
diff --git a/app/web/typings/webpack.d.ts b/app/web/typings/webpack.d.ts
new file mode 100644
index 0000000..357c0a8
--- /dev/null
+++ b/app/web/typings/webpack.d.ts
@@ -0,0 +1,8 @@
+declare var EASY_ENV_IS_NODE: boolean;
+declare var EASY_ENV_IS_BROWSER: boolean;
+declare var EASY_ENV_IS_DEV: boolean;
+declare var process : {
+ env: {
+ NODE_ENV: string
+ }
+}
\ No newline at end of file
diff --git a/app/web/typings/window.d.ts b/app/web/typings/window.d.ts
new file mode 100644
index 0000000..e69de29
diff --git a/app/web/view/layout.html b/app/web/view/layout.html
new file mode 100644
index 0000000..3c4e4d5
--- /dev/null
+++ b/app/web/view/layout.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..981e82b
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,15 @@
+environment:
+ matrix:
+ - nodejs_version: '8'
+ - nodejs_version: '10'
+
+install:
+ - ps: Install-Product node $env:nodejs_version
+ - npm i npminstall && node_modules\.bin\npminstall
+
+test_script:
+ - node --version
+ - npm --version
+ - npm run test
+
+build: off
diff --git a/bin/cli.js b/bin/cli.js
new file mode 100755
index 0000000..ceffc5e
--- /dev/null
+++ b/bin/cli.js
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+
+'use strict';
+const path = require('path');
+const fs = require('fs');
+const EggScriptCommand = require('egg-scripts');
+const cliFilePath = path.join(process.cwd(), 'node_modules', '@easy-team/res-cli');
+// fix when production mode, ves-cli not exists, start app error by ves start
+if (fs.existsSync(cliFilePath)) {
+ const ResCLI = require(cliFilePath);
+ new ResCLI.Command().start();
+} else {
+ new EggScriptCommand().start();
+}
diff --git a/config/config.default.js b/config/config.default.js
new file mode 100644
index 0000000..6b49990
--- /dev/null
+++ b/config/config.default.js
@@ -0,0 +1,38 @@
+'use strict';
+const path = require('path');
+module.exports = appInfo => {
+ const config = {};
+
+ config.view = {
+ defaultViewEngine: 'nunjucks',
+ mapping: {
+ '.tpl': 'nunjucks'
+ },
+ };
+
+ /**
+ * some description
+ * @member Config#test
+ * @property {String} key - some description
+ */
+ config.test = {
+ key: appInfo.name + '_123456',
+ };
+
+ /**
+ * cookie secret key
+ */
+ config.keys = appInfo.name + '_123456';
+
+ /**
+ * vue ssr plugin config
+ * @member Config#vuessr
+ * @property {String} layout - client render default html layout file path
+ * @property {renderOptions} Object - vue server side render options
+ */
+ config.reactssr = {
+ layout: path.resolve(__dirname, '../app/web/view/layout.html')
+ };
+
+ return config;
+};
diff --git a/config/config.local.js b/config/config.local.js
new file mode 100644
index 0000000..68a4582
--- /dev/null
+++ b/config/config.local.js
@@ -0,0 +1,31 @@
+'use strict';
+const utils = require('../lib/utils');
+module.exports = app => {
+
+ const exports = {};
+
+ exports.view = {
+ cache: false,
+ };
+
+ exports.static = {
+ maxAge: 0, // maxAge cache, default one year, local disabled
+ };
+
+ exports.development = {
+ watchDirs: [],
+ ignoreDirs: [ 'app/web', 'public', 'config/manifest.json' ],
+ };
+
+ /**
+ * egg-webpack plugin config
+ * @member Config#webpack
+ * @property {Array} webpackConfigList - webpack config
+ */
+ exports.webpack = {
+ cli: utils.getCli(),
+ webpackConfigList: utils.getWebpackConfig(app),
+ };
+
+ return exports;
+};
diff --git a/config/plugin.js b/config/plugin.js
new file mode 100644
index 0000000..ccf43e2
--- /dev/null
+++ b/config/plugin.js
@@ -0,0 +1,10 @@
+'use strict';
+exports.reactssr = {
+ enable: true,
+ package: 'egg-view-react-ssr',
+};
+
+exports.nunjucks = {
+ enable: true,
+ package: 'egg-view-nunjucks',
+};
\ No newline at end of file
diff --git a/config/plugin.local.js b/config/plugin.local.js
new file mode 100644
index 0000000..cc1fde5
--- /dev/null
+++ b/config/plugin.local.js
@@ -0,0 +1,15 @@
+'use strict';
+exports.cors = {
+ enable: true,
+ package: 'egg-cors',
+};
+
+exports.webpack = {
+ enable: true,
+ package: 'egg-webpack',
+};
+
+exports.webpackreact = {
+ enable: true,
+ package: 'egg-webpack-react',
+};
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..fb7b84c
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,3 @@
+import * as Egg from 'egg';
+export = Egg;
+export as namespace Egg;
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..1166f5f
--- /dev/null
+++ b/index.js
@@ -0,0 +1,3 @@
+'use strict';
+
+module.exports = require('./lib/framework.js');
diff --git a/lib/framework.js b/lib/framework.js
new file mode 100644
index 0000000..db18eb4
--- /dev/null
+++ b/lib/framework.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const path = require('path');
+const egg = require('egg');
+const EGG_PATH = Symbol.for('egg#eggPath');
+
+class Application extends egg.Application {
+ get [EGG_PATH]() {
+ return path.dirname(__dirname);
+ }
+}
+
+class Agent extends egg.Agent {
+ get [EGG_PATH]() {
+ return path.dirname(__dirname);
+ }
+}
+
+module.exports = Object.assign(egg, {
+ Application,
+ Agent,
+});
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..0fb3b15
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,16 @@
+'use strict';
+const path = require('path');
+const fs = require('fs');
+exports.getCli = () => {
+ return { name: 'res-cli', cmd: 'res', package: '@easy-team/res-cli' };
+};
+exports.getWebpackConfig = appInfo => {
+ const cli = exports.getCli();
+ const filepath = path.join(appInfo.baseDir, 'node_modules', cli.package);
+ const cwdFilepath = path.join(process.cwd(), 'node_modules', cli.package);
+ if (fs.existsSync(filepath) || fs.existsSync(cwdFilepath)) {
+ const resCli = require(cli.package);
+ return resCli.getWebpackConfig();
+ }
+ return [];
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..83ac89a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,73 @@
+{
+ "name": "@easy-team/res",
+ "version": "1.0.0",
+ "description": "A Powerful, Simple Node React Web Framework",
+ "dependencies": {
+ "egg": "^2.3.0",
+ "egg-cors": "^2.1.1",
+ "egg-scripts": "^2.10.0",
+ "egg-view-nunjucks": "^2.2.0",
+ "egg-view-react-ssr": "^2.5.0",
+ "react": "^16.0.0",
+ "react-dom": "^16.0.0"
+ },
+ "devDependencies": {
+ "autod": "^3.0.1",
+ "autod-egg": "^1.0.0",
+ "egg-bin": "^4.4.0",
+ "egg-ci": "^1.8.0",
+ "egg-mock": "^3.14.0",
+ "eslint": "^4.18.0",
+ "eslint-config-egg": "^7.0.0",
+ "webstorm-disable-index": "^1.2.0",
+ "conventional-changelog-cli": "^1.3.5"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "scripts": {
+ "test": "npm run lint -- --fix && egg-bin pkgfiles && npm run test-local",
+ "test-local": "egg-bin test --full-trace",
+ "cov": "egg-bin cov",
+ "lint": "eslint .",
+ "ci": "npm run lint && egg-bin pkgfiles --check && npm run cov",
+ "autod": "autod",
+ "pkgfiles": "egg-bin pkgfiles",
+ "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
+ },
+ "ci": {
+ "version": "8, 10, 12"
+ },
+ "repository": {
+ "type": "git",
+ "url": ""
+ },
+ "keywords": [
+ "egg",
+ "egg-framework",
+ "egg-webpack",
+ "res",
+ "react server render",
+ "react-Isomorphic",
+ "react-server-render"
+ ],
+ "author": "sky",
+ "files": [
+ "index.d.ts",
+ "index.js",
+ "bin",
+ "lib",
+ "app",
+ "config",
+ "app.js",
+ "index.d.ts"
+ ],
+ "eslintIgnore": [
+ "coverage",
+ "dist",
+ "test/fixtures/example/node_modules",
+ "test/fixtures/example/public",
+ "test/fixtures/example/app/view"
+ ],
+ "license": "MIT"
+}
diff --git a/test/fixtures/example/app/controller/home.js b/test/fixtures/example/app/controller/home.js
new file mode 100644
index 0000000..6884699
--- /dev/null
+++ b/test/fixtures/example/app/controller/home.js
@@ -0,0 +1,18 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class HomeController extends Controller {
+ async index() {
+ const data = await this.service.test.get(123);
+ this.ctx.body = data.name;
+ }
+ async render() {
+ await this.ctx.render('home/home.js', { message: 'res server side render' });
+ }
+ async renderClient() {
+ await this.ctx.renderClient('home/home.js', { message: 'res client side render' });
+ }
+}
+
+module.exports = HomeController;
diff --git a/test/fixtures/example/app/router.js b/test/fixtures/example/app/router.js
new file mode 100644
index 0000000..f368bf2
--- /dev/null
+++ b/test/fixtures/example/app/router.js
@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = app => {
+ const { router, controller } = app;
+ router.get('/', controller.home.index);
+ router.get('/render', controller.home.render);
+ router.get('/renderClient', controller.home.renderClient);
+};
diff --git a/test/fixtures/example/app/view/home/home.js b/test/fixtures/example/app/view/home/home.js
new file mode 100644
index 0000000..39d2297
--- /dev/null
+++ b/test/fixtures/example/app/view/home/home.js
@@ -0,0 +1,11563 @@
+(function(e, a) { for(var i in a) e[i] = a[i]; }(exports, /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "/public/";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 4);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports) {
+
+exports.sync = function (store, router, options) {
+ var moduleName = (options || {}).moduleName || 'route'
+
+ store.registerModule(moduleName, {
+ namespaced: true,
+ state: cloneRoute(router.currentRoute),
+ mutations: {
+ 'ROUTE_CHANGED': function ROUTE_CHANGED (state, transition) {
+ store.state[moduleName] = cloneRoute(transition.to, transition.from)
+ }
+ }
+ })
+
+ var isTimeTraveling = false
+ var currentPath
+
+ // sync router on store change
+ var storeUnwatch = store.watch(
+ function (state) { return state[moduleName]; },
+ function (route) {
+ var fullPath = route.fullPath;
+ if (fullPath === currentPath) {
+ return
+ }
+ if (currentPath != null) {
+ isTimeTraveling = true
+ router.push(route)
+ }
+ currentPath = fullPath
+ },
+ { sync: true }
+ )
+
+ // sync store on router navigation
+ var afterEachUnHook = router.afterEach(function (to, from) {
+ if (isTimeTraveling) {
+ isTimeTraveling = false
+ return
+ }
+ currentPath = to.fullPath
+ store.commit(moduleName + '/ROUTE_CHANGED', { to: to, from: from })
+ })
+
+ return function unsync () {
+ // On unsync, remove router hook
+ if (afterEachUnHook != null) {
+ afterEachUnHook()
+ }
+
+ // On unsync, remove store watch
+ if (storeUnwatch != null) {
+ storeUnwatch()
+ }
+
+ // On unsync, unregister Module with store
+ store.unregisterModule(moduleName)
+ }
+}
+
+function cloneRoute (to, from) {
+ var clone = {
+ name: to.name,
+ path: to.path,
+ hash: to.hash,
+ query: to.query,
+ params: to.params,
+ fullPath: to.fullPath,
+ meta: to.meta
+ }
+ if (from) {
+ clone.from = cloneRoute(from)
+ }
+ return Object.freeze(clone)
+}
+
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+// style-loader: Adds some css to the DOM by adding a '
+ }
+ return css
+}
+
+
+/***/ })
+/******/ ])));
\ No newline at end of file
diff --git a/test/fixtures/example/app/web/componet/layout.tsx b/test/fixtures/example/app/web/componet/layout.tsx
new file mode 100644
index 0000000..bcbb63f
--- /dev/null
+++ b/test/fixtures/example/app/web/componet/layout.tsx
@@ -0,0 +1,19 @@
+import React, { Component } from 'react';
+export default class Layout extends Component {
+ render() {
+ if(EASY_ENV_IS_NODE) {
+ return
+
+ {this.props.title}
+
+
+
+
+
+
+ {this.props.children}
+ ;
+ }
+ return this.props.children;
+ }
+}
\ No newline at end of file
diff --git a/test/fixtures/example/app/web/framework/app.tsx b/test/fixtures/example/app/web/framework/app.tsx
new file mode 100644
index 0000000..c1119f7
--- /dev/null
+++ b/test/fixtures/example/app/web/framework/app.tsx
@@ -0,0 +1,22 @@
+import React, { Component } from 'react';
+import ReactDOM from 'react-dom';
+
+const clientRender = (Com, method) => {
+ const state = window.__INITIAL_STATE__;
+ const root = document.getElementById('app');
+ ReactDOM[method](, root);
+};
+
+const serverRender = Com => {
+ return Com;
+};
+
+export function SSR(Com) {
+ return EASY_ENV_IS_NODE ? serverRender(Com) : clientRender(Com, 'hydrate');
+}
+
+export function CSR(Com) {
+ return clientRender(Com, 'render');
+}
+
+
diff --git a/test/fixtures/example/app/web/page/react-client-render/index.tsx b/test/fixtures/example/app/web/page/react-client-render/index.tsx
new file mode 100644
index 0000000..1a6adbb
--- /dev/null
+++ b/test/fixtures/example/app/web/page/react-client-render/index.tsx
@@ -0,0 +1,15 @@
+import React, { Component } from 'react';
+import Layout from '../../component/layout.jsx';
+import { CSR } from '../../framework/app';
+
+class Index extends Component {
+ componentDidMount() {
+ console.log('client render');
+ }
+ render() {
+ return {this.props.text}
;
+ }
+}
+
+
+export default CSR(Index);
\ No newline at end of file
diff --git a/test/fixtures/example/app/web/page/react-nunjucks-render/component/index.tsx b/test/fixtures/example/app/web/page/react-nunjucks-render/component/index.tsx
new file mode 100644
index 0000000..ffa96e9
--- /dev/null
+++ b/test/fixtures/example/app/web/page/react-nunjucks-render/component/index.tsx
@@ -0,0 +1,46 @@
+import React, { Component, CSSProperties } from 'react';
+import { observable } from 'mobx';
+import { Observer, observer, inject } from 'mobx-react';
+
+const info = observable({ text: "Mobx Observer Local Test" });
+
+class LocalReactive extends Component {
+
+ style: CSSProperties = {
+ textAlign: 'center',
+ fontSize: '20px',
+ color: 'red',
+ marginTop: '24px'
+ }
+
+ handle() {
+ info.text = `${info.text } Local Update`;
+ console.log('click');
+ }
+
+ render() {
+ return
+
{info.text}
+
{() => { this.handle()}}>点我:{info.text}
}
+
+ }
+}
+
+@inject('appStore')
+@observer
+class MobXApp extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const { appStore } = this.props;
+ // [mobx] Dynamic observable objects cannot be frozen https://github.com/mobxjs/mobx/blob/master/CHANGELOG.md
+ return
+
{appStore.state.text}
+
{ appStore.plus()}}>点我:{appStore.number}
+
+
+ }
+};
+export default MobXApp;
\ No newline at end of file
diff --git a/test/fixtures/example/app/web/page/react-nunjucks-render/index.tsx b/test/fixtures/example/app/web/page/react-nunjucks-render/index.tsx
new file mode 100644
index 0000000..431fc8b
--- /dev/null
+++ b/test/fixtures/example/app/web/page/react-nunjucks-render/index.tsx
@@ -0,0 +1,22 @@
+
+import React, { Component } from 'react';
+import { Provider } from 'mobx-react';
+import AppStore from './store';
+import MobXApp from './component';
+import { CSR } from '../../framework/app'
+
+const stores = window.stores = window.stores || {
+ appStore: new AppStore()
+};
+
+class Index extends Component {
+ componentDidMount() {
+ console.log('Nunjucks Render');
+ }
+ render() {
+ return;
+ }
+}
+
+
+export default CSR(Index);
\ No newline at end of file
diff --git a/test/fixtures/example/app/web/page/react-nunjucks-render/store/index.tsx b/test/fixtures/example/app/web/page/react-nunjucks-render/store/index.tsx
new file mode 100644
index 0000000..29fed53
--- /dev/null
+++ b/test/fixtures/example/app/web/page/react-nunjucks-render/store/index.tsx
@@ -0,0 +1,23 @@
+import { CSSProperties } from 'react';
+import { observable, action } from 'mobx';
+class AppStore {
+ @observable state = window.__INITIAL_STATE__ || {};
+ // Test 定义状态并使其可观察
+ @observable number = 0;
+ @observable size = 24;
+ @observable style: CSSProperties ={
+ textAlign: 'center',
+ marginTop: '40px',
+ fontSize: `${this.size}px`
+ }
+
+ @action
+ plus(){
+ this.number++;
+ this.style = {
+ ...this.style,
+ fontSize: `${this.size++}px`
+ }
+ }
+}
+export default AppStore;
\ No newline at end of file
diff --git a/test/fixtures/example/app/web/page/react-server-render/index.tsx b/test/fixtures/example/app/web/page/react-server-render/index.tsx
new file mode 100644
index 0000000..8143dc8
--- /dev/null
+++ b/test/fixtures/example/app/web/page/react-server-render/index.tsx
@@ -0,0 +1,17 @@
+import React, { Component } from 'react';
+import Layout from '../../component/layout';
+import { SSR } from '../../framework/app';
+
+class Index extends Component {
+ componentDidMount() {
+ console.log('render');
+ }
+ render() {
+ return
+ {this.props.text}
+ ;
+ }
+}
+
+
+export default SSR(Index);
\ No newline at end of file
diff --git a/test/fixtures/example/app/web/view/layout.html b/test/fixtures/example/app/web/view/layout.html
new file mode 100644
index 0000000..3c4e4d5
--- /dev/null
+++ b/test/fixtures/example/app/web/view/layout.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/example/config/config.default.js b/test/fixtures/example/config/config.default.js
new file mode 100644
index 0000000..853091e
--- /dev/null
+++ b/test/fixtures/example/config/config.default.js
@@ -0,0 +1,6 @@
+'use strict';
+// const path = require('path');
+module.exports = () => {
+ const config = {};
+ return config;
+};
diff --git a/test/fixtures/example/config/config.unittest.js b/test/fixtures/example/config/config.unittest.js
new file mode 100644
index 0000000..a6ada2b
--- /dev/null
+++ b/test/fixtures/example/config/config.unittest.js
@@ -0,0 +1,3 @@
+'use strict';
+
+// exports.keys = '123456';
diff --git a/test/fixtures/example/config/manifest.json b/test/fixtures/example/config/manifest.json
new file mode 100644
index 0000000..1549a57
--- /dev/null
+++ b/test/fixtures/example/config/manifest.json
@@ -0,0 +1,21 @@
+{
+ "home/home.css": "/public/css/0.03cd5845.css",
+ "home/home.js": "/public/js/chunk/home/home.cd531b5c.js",
+ "runtime.js": "/public/js/runtime.49eef78a.js",
+ "deps": {
+ "home/home.js": {
+ "js": [
+ "/public/js/runtime.49eef78a.js",
+ "/public/js/chunk/home/home.cd531b5c.js"
+ ],
+ "css": [
+ "/public/css/0.03cd5845.css"
+ ]
+ }
+ },
+ "info": {
+ "publicPath": "/public/",
+ "buildPath": "public",
+ "mapped": true
+ }
+}
\ No newline at end of file
diff --git a/test/fixtures/example/config/plugin.js b/test/fixtures/example/config/plugin.js
new file mode 100644
index 0000000..fb08d05
--- /dev/null
+++ b/test/fixtures/example/config/plugin.js
@@ -0,0 +1,5 @@
+// 'use strict';
+// exports.vuessr = {
+// enable: true,
+// package: 'egg-view-vue-ssr',
+// };
diff --git a/test/fixtures/example/package.json b/test/fixtures/example/package.json
new file mode 100644
index 0000000..3cd5c16
--- /dev/null
+++ b/test/fixtures/example/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "framework-example",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "egg-bin dev",
+ "start": "egg-scripts start"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "axios": "^0.17.1",
+ "egg": "^2.3.0",
+ "egg-scripts": "^2.10.0",
+ "egg-cors": "^2.1.1",
+ "egg-webpack": "^4.4.7",
+ "egg-webpack-vue": "^2.0.0",
+ "egg-view-vue-ssr": "^3.0.5",
+ "vue": "^2.5.0",
+ "vue-router": "^3.0.1",
+ "vuex": "^3.0.1",
+ "vuex-router-sync": "^5.0.0"
+ }
+}
diff --git a/test/fixtures/example/public/css/0.03cd5845.css b/test/fixtures/example/public/css/0.03cd5845.css
new file mode 100644
index 0000000..e3c0074
--- /dev/null
+++ b/test/fixtures/example/public/css/0.03cd5845.css
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/example/public/js/chunk/home/home.cd531b5c.js b/test/fixtures/example/public/js/chunk/home/home.cd531b5c.js
new file mode 100644
index 0000000..1c818ad
--- /dev/null
+++ b/test/fixtures/example/public/js/chunk/home/home.cd531b5c.js
@@ -0,0 +1,11636 @@
+(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
+
+/***/ "UQ5/":
+/***/ (function(module, exports) {
+
+var g;
+
+// This works in non-strict mode
+g = (function() {
+ return this;
+})();
+
+try {
+ // This works if eval is allowed (see CSP)
+ g = g || Function("return this")() || (1, eval)("this");
+} catch (e) {
+ // This works if the window reference is available
+ if (typeof window === "object") g = window;
+}
+
+// g can still be undefined, but nothing to do about it...
+// We return undefined, instead of nothing here, so it's
+// easier to handle this case. if(!global) { ...}
+
+module.exports = g;
+
+
+/***/ }),
+
+/***/ "Wzic":
+/***/ (function(module, exports, __webpack_require__) {
+
+// extracted by mini-css-extract-plugin
+
+/***/ }),
+
+/***/ "iX8O":
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+
+// EXTERNAL MODULE: ../ves/node_modules/_vue@2.5.17@vue/dist/vue.esm.js
+var vue_esm = __webpack_require__("tgp/");
+
+// CONCATENATED MODULE: ../ves/test/fixtures/example/node_modules/babel-loader/lib?{"cacheDirectory":"/var/folders/hj/fs_pkpfn7vdcg176058mb1540000gn/T/easywebpack/framework-example/test/cache/client/babel-loader","env":{"node":{"presets":[["env",{"modules":false,"targets":{"node":"current"}}]],"plugins":["transform-object-rest-spread","syntax-dynamic-import"]},"web":{"presets":[["env",{"modules":false,"targets":{"browsers":["last 2 versions","safari >= 7"]}}]],"plugins":["transform-object-rest-spread","syntax-dynamic-import","transform-object-assign","transform-runtime"]}},"comments":false}!../ves/test/fixtures/example/node_modules/vue-loader/lib/selector.js?type=script&index=0!../ves/test/fixtures/example/app/web/page/home/home.vue
+
+
+/* harmony default export */ var home = ({});
+// CONCATENATED MODULE: ../ves/test/fixtures/example/node_modules/vue-loader/lib/template-compiler?{"id":"data-v-42669c5e","hasScoped":false,"optionsId":"1","buble":{"transforms":{}}}!../ves/test/fixtures/example/node_modules/vue-loader/lib/selector.js?type=template&index=0!../ves/test/fixtures/example/app/web/page/home/home.vue
+var render = function() {
+ var _vm = this
+ var _h = _vm.$createElement
+ var _c = _vm._self._c || _h
+ return _c("div", [
+ _c("div", [_vm._v("ves -- The Vue Isomorphic Node Framework")]),
+ _vm._v(" "),
+ _c("h1", [_vm._v(_vm._s(_vm.message))])
+ ])
+}
+var staticRenderFns = []
+render._withStripped = true
+
+if (false) {}
+// CONCATENATED MODULE: ../ves/test/fixtures/example/node_modules/vue-loader/lib/runtime/component-normalizer.js
+/* globals __VUE_SSR_CONTEXT__ */
+
+// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).
+// This module is a runtime utility for cleaner component module output and will
+// be included in the final webpack user bundle.
+
+function normalizeComponent (
+ scriptExports,
+ render,
+ staticRenderFns,
+ functionalTemplate,
+ injectStyles,
+ scopeId,
+ moduleIdentifier, /* server only */
+ shadowMode /* vue-cli only */
+) {
+ scriptExports = scriptExports || {}
+
+ // ES6 modules interop
+ var type = typeof scriptExports.default
+ if (type === 'object' || type === 'function') {
+ scriptExports = scriptExports.default
+ }
+
+ // Vue.extend constructor export interop
+ var options = typeof scriptExports === 'function'
+ ? scriptExports.options
+ : scriptExports
+
+ // render functions
+ if (render) {
+ options.render = render
+ options.staticRenderFns = staticRenderFns
+ options._compiled = true
+ }
+
+ // functional template
+ if (functionalTemplate) {
+ options.functional = true
+ }
+
+ // scopedId
+ if (scopeId) {
+ options._scopeId = scopeId
+ }
+
+ var hook
+ if (moduleIdentifier) { // server build
+ hook = function (context) {
+ // 2.3 injection
+ context =
+ context || // cached call
+ (this.$vnode && this.$vnode.ssrContext) || // stateful
+ (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
+ // 2.2 with runInNewContext: true
+ if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
+ context = __VUE_SSR_CONTEXT__
+ }
+ // inject component styles
+ if (injectStyles) {
+ injectStyles.call(this, context)
+ }
+ // register component module identifier for async chunk inferrence
+ if (context && context._registeredComponents) {
+ context._registeredComponents.add(moduleIdentifier)
+ }
+ }
+ // used by ssr in case component is cached and beforeCreate
+ // never gets called
+ options._ssrRegister = hook
+ } else if (injectStyles) {
+ hook = shadowMode
+ ? function () { injectStyles.call(this, this.$root.$options.shadowRoot) }
+ : injectStyles
+ }
+
+ if (hook) {
+ if (options.functional) {
+ // for template-only hot-reload because in that case the render fn doesn't
+ // go through the normalizer
+ options._injectStyles = hook
+ // register for functioal component in vue file
+ var originalRender = options.render
+ options.render = function renderWithStyleInjection (h, context) {
+ hook.call(context)
+ return originalRender(h, context)
+ }
+ } else {
+ // inject component registration as beforeCreate hook
+ var existing = options.beforeCreate
+ options.beforeCreate = existing
+ ? [].concat(existing, hook)
+ : [hook]
+ }
+ }
+
+ return {
+ exports: scriptExports,
+ options: options
+ }
+}
+
+// CONCATENATED MODULE: ../ves/test/fixtures/example/app/web/page/home/home.vue
+var disposed = false
+function injectStyle (context) {
+ if (disposed) return
+ __webpack_require__("Wzic")
+}
+/* script */
+
+
+/* template */
+
+/* template functional */
+var __vue_template_functional__ = false
+/* styles */
+var __vue_styles__ = injectStyle
+/* scopeId */
+var __vue_scopeId__ = null
+/* moduleIdentifier (server only) */
+var __vue_module_identifier__ = null
+
+var Component = normalizeComponent(
+ home,
+ render,
+ staticRenderFns,
+ __vue_template_functional__,
+ __vue_styles__,
+ __vue_scopeId__,
+ __vue_module_identifier__
+)
+Component.options.__file = "ves/test/fixtures/example/app/web/page/home/home.vue"
+
+/* hot reload */
+if (false) {}
+
+/* harmony default export */ var home_home = (Component.exports);
+
+// CONCATENATED MODULE: ../ves/test/fixtures/example/node_modules/babel-loader/lib?{"cacheDirectory":"/var/folders/hj/fs_pkpfn7vdcg176058mb1540000gn/T/easywebpack/framework-example/test/cache/client/babel-loader","env":{"node":{"presets":[["env",{"modules":false,"targets":{"node":"current"}}]],"plugins":["transform-object-rest-spread","syntax-dynamic-import"]},"web":{"presets":[["env",{"modules":false,"targets":{"browsers":["last 2 versions","safari >= 7"]}}]],"plugins":["transform-object-rest-spread","syntax-dynamic-import","transform-object-assign","transform-runtime"]}},"comments":false,"babelrc":"/Users/caoli/dev/github/ves/ves/test/fixtures/example/node_modules/easywebpack-vue/config/.babelrc"}!../ves/test/fixtures/example/node_modules/vue-entry-loader?templateFile=!../ves/test/fixtures/example/app/web/page/home/home.vue
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+
+
+
+const data = window.__INITIAL_STATE__ || {};
+const context = { state: data };
+const hook = home_home.hook || vue_esm["a" /* default */].hook || {};
+const home_render = hook.render || home_home.hookRender || vue_esm["a" /* default */].hookRender;
+if (home_render) {
+ home_render(context, home_home);
+}
+const store = typeof home_home.store === 'function' ? home_home.store(data) : home_home.store;
+const router = typeof home_home.router === 'function' ? home_home.router() : home_home.router;
+const options = store && router ? _extends({}, home_home, {
+ store,
+ router
+}) : _extends({}, home_home, { data });
+const app = new vue_esm["a" /* default */](options);
+app.$mount('#app');
+
+/***/ }),
+
+/***/ "oPdv":
+/***/ (function(module, exports, __webpack_require__) {
+
+/* WEBPACK VAR INJECTION */(function(global, process) {(function (global, undefined) {
+ "use strict";
+
+ if (global.setImmediate) {
+ return;
+ }
+
+ var nextHandle = 1; // Spec says greater than zero
+ var tasksByHandle = {};
+ var currentlyRunningATask = false;
+ var doc = global.document;
+ var registerImmediate;
+
+ function setImmediate(callback) {
+ // Callback can either be a function or a string
+ if (typeof callback !== "function") {
+ callback = new Function("" + callback);
+ }
+ // Copy function arguments
+ var args = new Array(arguments.length - 1);
+ for (var i = 0; i < args.length; i++) {
+ args[i] = arguments[i + 1];
+ }
+ // Store and register the task
+ var task = { callback: callback, args: args };
+ tasksByHandle[nextHandle] = task;
+ registerImmediate(nextHandle);
+ return nextHandle++;
+ }
+
+ function clearImmediate(handle) {
+ delete tasksByHandle[handle];
+ }
+
+ function run(task) {
+ var callback = task.callback;
+ var args = task.args;
+ switch (args.length) {
+ case 0:
+ callback();
+ break;
+ case 1:
+ callback(args[0]);
+ break;
+ case 2:
+ callback(args[0], args[1]);
+ break;
+ case 3:
+ callback(args[0], args[1], args[2]);
+ break;
+ default:
+ callback.apply(undefined, args);
+ break;
+ }
+ }
+
+ function runIfPresent(handle) {
+ // From the spec: "Wait until any invocations of this algorithm started before this one have completed."
+ // So if we're currently running a task, we'll need to delay this invocation.
+ if (currentlyRunningATask) {
+ // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a
+ // "too much recursion" error.
+ setTimeout(runIfPresent, 0, handle);
+ } else {
+ var task = tasksByHandle[handle];
+ if (task) {
+ currentlyRunningATask = true;
+ try {
+ run(task);
+ } finally {
+ clearImmediate(handle);
+ currentlyRunningATask = false;
+ }
+ }
+ }
+ }
+
+ function installNextTickImplementation() {
+ registerImmediate = function(handle) {
+ process.nextTick(function () { runIfPresent(handle); });
+ };
+ }
+
+ function canUsePostMessage() {
+ // The test against `importScripts` prevents this implementation from being installed inside a web worker,
+ // where `global.postMessage` means something completely different and can't be used for this purpose.
+ if (global.postMessage && !global.importScripts) {
+ var postMessageIsAsynchronous = true;
+ var oldOnMessage = global.onmessage;
+ global.onmessage = function() {
+ postMessageIsAsynchronous = false;
+ };
+ global.postMessage("", "*");
+ global.onmessage = oldOnMessage;
+ return postMessageIsAsynchronous;
+ }
+ }
+
+ function installPostMessageImplementation() {
+ // Installs an event handler on `global` for the `message` event: see
+ // * https://developer.mozilla.org/en/DOM/window.postMessage
+ // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
+
+ var messagePrefix = "setImmediate$" + Math.random() + "$";
+ var onGlobalMessage = function(event) {
+ if (event.source === global &&
+ typeof event.data === "string" &&
+ event.data.indexOf(messagePrefix) === 0) {
+ runIfPresent(+event.data.slice(messagePrefix.length));
+ }
+ };
+
+ if (global.addEventListener) {
+ global.addEventListener("message", onGlobalMessage, false);
+ } else {
+ global.attachEvent("onmessage", onGlobalMessage);
+ }
+
+ registerImmediate = function(handle) {
+ global.postMessage(messagePrefix + handle, "*");
+ };
+ }
+
+ function installMessageChannelImplementation() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = function(event) {
+ var handle = event.data;
+ runIfPresent(handle);
+ };
+
+ registerImmediate = function(handle) {
+ channel.port2.postMessage(handle);
+ };
+ }
+
+ function installReadyStateChangeImplementation() {
+ var html = doc.documentElement;
+ registerImmediate = function(handle) {
+ // Create a