├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── no-response.yml ├── pull_request_template.md └── workflows │ ├── cov.yml │ ├── release.yml │ ├── testOnLinux.yml │ ├── testOnMac.yml │ └── testOnWin.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── codecov.yml ├── commitlint.config.js ├── examples ├── eruda │ └── README.md ├── portal │ └── README.md ├── proxy │ ├── .svrxrc.js │ ├── README.md │ └── public │ │ ├── index.html │ │ └── main.js ├── serve-static-page │ ├── .svrxrc.js │ ├── README.md │ └── public │ │ ├── index.html │ │ └── main.js └── weinre │ └── README.md ├── lerna.json ├── package.json └── packages ├── svrx-util ├── .npmrc ├── README.md ├── __tests__ │ ├── fixture │ │ ├── .svrx │ │ │ ├── config │ │ │ │ └── .svrxrc.js │ │ │ ├── plugins │ │ │ │ └── hello │ │ │ │ │ ├── 0.0.5 │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ │ │ ├── 1.0.0 │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ │ │ └── 1.0.1 │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ └── versions │ │ │ │ └── 1.0.6 │ │ │ │ ├── index.js │ │ │ │ ├── lib │ │ │ │ └── svrx.js │ │ │ │ └── package.json │ │ └── plugin │ │ │ ├── svrx-plugin-error-no-version │ │ │ ├── index.js │ │ │ └── package.json │ │ │ ├── svrx-plugin-no-package │ │ │ └── index.js │ │ │ └── svrx-plugin-test │ │ │ ├── assets │ │ │ ├── index.css │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── package.json │ ├── spec │ │ ├── logger.test.js │ │ ├── name-formatter.test.js │ │ ├── package-manager.test.js │ │ └── rc-file-read.test.js │ └── svrx-util.test.js ├── lib │ ├── c2k.js │ ├── logger.js │ ├── name-formatter.js │ ├── npCall │ │ └── index.js │ ├── package-manager │ │ ├── index.js │ │ ├── package-manager.js │ │ └── plugin-package-manager.js │ ├── rc-file-read.js │ └── svrx-util.js └── package.json └── svrx ├── .npmrc ├── README.md ├── __tests__ ├── fixture │ ├── plugin │ │ ├── historyApiFallback │ │ │ └── index.html │ │ ├── serve │ │ │ ├── demo.html │ │ │ ├── demo.js │ │ │ └── demo.png │ │ ├── svrx-plugin-depend │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── svrx-plugin-no-package │ │ │ └── index.js │ │ └── svrx-plugin-test │ │ │ ├── assets │ │ │ ├── index.css │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── package.json │ └── router │ │ ├── rule.normal.js │ │ ├── rule.require.js │ │ └── static │ │ └── index.html ├── spec │ ├── configure │ │ ├── builtinConfig.js │ │ ├── cli.js │ │ ├── inline.js │ │ ├── pluginConfig.js │ │ └── ui.js │ ├── svrx.base.js │ ├── svrx.injector.js │ ├── svrx.io.js │ ├── svrx.plugin.js │ ├── svrx.plugin.proxy.js │ ├── svrx.router.js │ └── svrx.utility.js └── util.js ├── index.js ├── lib ├── config-list.js ├── configure │ ├── builtinOption.js │ ├── index.js │ ├── option.js │ ├── plugin.js │ ├── pluginInfo.js │ └── pluginOption.js ├── constant.js ├── injector │ ├── client │ │ └── index.js │ ├── index.js │ └── replace.js ├── io │ ├── client.js │ └── index.js ├── manager.js ├── middleware │ └── index.js ├── model.js ├── plugin │ ├── loader.js │ ├── svrx-plugin-cors │ │ └── index.js │ ├── svrx-plugin-history-api-fallback │ │ └── index.js │ ├── svrx-plugin-livereload │ │ ├── assets │ │ │ └── index.js │ │ └── index.js │ ├── svrx-plugin-open │ │ └── index.js │ ├── svrx-plugin-proxy │ │ └── index.js │ ├── svrx-plugin-serve │ │ └── index.js │ └── system.js ├── router │ ├── actions.js │ ├── index.js │ ├── loader.js │ ├── methods.js │ ├── route.js │ └── router.js ├── shared │ ├── cache.js │ ├── consts.js │ ├── events.js │ └── uid.js ├── svrx.js └── util │ ├── compose.js │ ├── gzip.js │ ├── helper.js │ ├── hmr.js │ ├── im.js │ ├── jsonSchemaDefaults.js │ ├── logger.js │ └── semver.js ├── package.json ├── resource └── cert │ ├── rootCA.crt │ └── rootCA.key └── scripts ├── gen.cert.js └── webpack.injector.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages/**/assets/**/*.js 3 | packages/**/dist/**/*.js 4 | /**/fixture 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@svrx", 3 | "overrides": [ 4 | { 5 | "files": "examples/**/*.js", 6 | "rules": { 7 | "no-unused-vars": "off" 8 | } 9 | }, { 10 | "files": "packages/**/__tests__/**/*.js", 11 | "rules": { 12 | "import/no-extraneous-dependencies": "off" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | 29 | - OS: [e.g. iOS] 30 | - Node: [e.g. 10.0.0, 12.0.0] 31 | - Browser: [e.g. chrome, safari] 32 | - Version: [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 14 5 | # Label requiring a response 6 | responseRequiredLabel: more-information-needed 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | information that is currently in the issue, we don't have enough information 12 | to take action. Please reach out if you have or find the answers we need so 13 | that we can investigate further. 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Please check if the PR fulfills these requirements 4 | 5 | - [ ] The commit message follows our guidelines 6 | - [x] Tests is needed? 7 | - [ ] Tests for the changes have been added 8 | - [x] Docs is needed? 9 | - [ ] Docs have been added / updated 10 | 11 | ## What kind of change does this PR introduce? 12 | 13 | - [ ] Bug fix 14 | - [ ] Feature 15 | - [ ] Enhencement 16 | - [ ] Refactor 17 | - [ ] Documents 18 | - [ ] Others 19 | 20 | 21 | 22 | 1. 23 | 1. 24 | 25 | ## What is the related issue? 26 | 27 | 28 | 29 | 30 | ## Does this PR introduce a breaking change? 31 | 32 | - [ ] breaking change? 33 | 34 | 35 | 36 | ## Other information: 37 | -------------------------------------------------------------------------------- /.github/workflows/cov.yml: -------------------------------------------------------------------------------- 1 | name: cov 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '12.x' 19 | - run: npm install 20 | - run: npm run build --if-present 21 | - run: npm test 22 | - run: npm run report-coverage 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release to npm 2 | on: 3 | push: 4 | branches: 5 | - 'release/**' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 # to fetch git tags 13 | # Setup .npmrc file to publish to npm 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: '12.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: npm install 19 | - run: npm run build 20 | - run: npm run lerna:publish 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/testOnLinux.yml: -------------------------------------------------------------------------------- 1 | name: test on linux 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [8.9.x, 12.x, 15.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm run build --if-present 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.github/workflows/testOnMac.yml: -------------------------------------------------------------------------------- 1 | name: test on macos 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: macos-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [8.9.x, 12.x, 15.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm run build --if-present 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.github/workflows/testOnWin.yml: -------------------------------------------------------------------------------- 1 | name: test on windows 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: windows-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [8.9.x, 12.x, 15.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm run build --if-present 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.lcov 2 | package-lock.json 3 | npm-shrinkwrap.json 4 | 5 | # Created by https://www.gitignore.io/api/node 6 | # Edit at https://www.gitignore.io/?templates=node 7 | 8 | ### Node ### 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | # End of https://www.gitignore.io/api/node 91 | 92 | tmp/ 93 | 94 | .DS_Store 95 | .changelog 96 | .vscode 97 | .idea 98 | 99 | # ignore dist 100 | 101 | dist 102 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.1.7 (2021-04-22) 2 | 3 | #### :bug: Bug Fix 4 | * `svrx-util` 5 | * Fix package install error on Windows ([@xuchaoying](https://github.com/xuchaoying)) 6 | 7 | ## v1.1.6 (2021-03-25) 8 | 9 | #### :bug: Bug Fix 10 | * `svrx` 11 | * [#195](https://github.com/svrxjs/svrx/pull/195) Fix start error on node@15, use child_process to exec npm commands directly ([@xuchaoying](https://github.com/xuchaoying)) 12 | 13 | ## v1.1.5 (2020-04-13) 14 | 15 | #### :rocket: New Feature 16 | * `svrx` 17 | * [#178](https://github.com/svrxjs/svrx/pull/178) Support lib require in route file ([@xuchaoying](https://github.com/xuchaoying)) 18 | 19 | #### :bug: Bug Fix 20 | * `svrx` 21 | * [#177](https://github.com/svrxjs/svrx/pull/177) Fix enable dashed plugins through cli shortcut ([@xuchaoying](https://github.com/xuchaoying)) 22 | 23 | ## v1.1.4 (2019-12-24) 24 | 25 | #### :rocket: New Feature 26 | * `svrx` 27 | * [#168](https://github.com/svrxjs/svrx/pull/168) Support svrx-plugin-ui ([@xuchaoying](https://github.com/xuchaoying)) 28 | 29 | #### :bug: Bug Fix 30 | * `svrx` 31 | * [#173](https://github.com/svrxjs/svrx/pull/173) Fix the priority of cors and route ([@xuchaoying](https://github.com/xuchaoying)) 32 | * [#167](https://github.com/svrxjs/svrx/pull/167) Fix `window.__svrx__` undefined when using require.js ([@xuchaoying](https://github.com/xuchaoying)) 33 | * `svrx-util`, `svrx` 34 | * [#165](https://github.com/svrxjs/svrx/pull/165) Revert global-npm to 0.3.0 ([@xuchaoying](https://github.com/xuchaoying)) 35 | 36 | #### :nail_care: Enhancement 37 | * `svrx-util` 38 | * [#174](https://github.com/svrxjs/svrx/pull/174) Put auto update package after local package load ([@xuchaoying](https://github.com/xuchaoying)) 39 | 40 | ## v1.1.3 (2019-12-02) 41 | 42 | #### :bug: Bug Fix 43 | * `svrx-util` 44 | * [#165](https://github.com/svrxjs/svrx/pull/165) Fix `Error: Cannot find module 'npm'` when starting svrx core from npm scripts ([@xuchaoying](https://github.com/xuchaoying)) 45 | 46 | ## v1.1.2 (2019-12-02) 47 | 48 | ## v1.1.1 (2019-11-28) 49 | 50 | #### :rocket: New Feature 51 | * `svrx-util` 52 | * [#159](https://github.com/svrxjs/svrx/pull/159) Add autoclean to package-manager ([@xuchaoying](https://github.com/xuchaoying)) 53 | 54 | ## v1.1.0 (2019-11-18) 55 | 56 | #### :rocket: New Feature 57 | * `svrx-util`, `svrx` 58 | * [#152](https://github.com/svrxjs/svrx/pull/152) Change plugin install strategy to global install ([@xuchaoying](https://github.com/xuchaoying)) 59 | 60 | Now the default path for plugin packages is changed from local `node_modules` in your project to `~/.svrx/plugins`. 61 | svrx still supports local plugin install, you can use `npm install --save-dev svrx-plugin-name` to save a plugin to your working directory. 62 | 63 | #### :bug: Bug Fix 64 | * `svrx` 65 | * [#151](https://github.com/svrxjs/svrx/pull/151) Upgrade the priority of built-in api ([@leeluolee](https://github.com/leeluolee)) 66 | 67 | ## v1.0.7 (2019-11-08) 68 | 69 | #### :bug: Bug Fix 70 | * `svrx-util` 71 | * [#148](https://github.com/svrxjs/svrx/pull/148) Replace global-npm.install with npminstall ([@xuchaoying](https://github.com/xuchaoying)) 72 | 73 | ## v1.0.6 (2019-11-01) 74 | 75 | #### :bug: Bug Fix 76 | * `svrx` 77 | * [#145](https://github.com/svrxjs/svrx/pull/145) Fix windows path parse error ([@int64ago](https://github.com/int64ago)) 78 | * [#143](https://github.com/svrxjs/svrx/pull/143) Fix livereload client error ([@leeluolee](https://github.com/leeluolee)) 79 | 80 | ## v1.0.5 (2019-10-29) 81 | 82 | #### :bug: Bug Fix 83 | * `svrx-util`, `svrx` 84 | * [#135](https://github.com/svrxjs/svrx/pull/135) Fix not working on Windows ([@xuchaoying](https://github.com/xuchaoying)) 85 | * `svrx-util` 86 | * [#134](https://github.com/svrxjs/svrx/pull/134) Fix wrong log display for scoped plugin ([@int64ago](https://github.com/int64ago)) 87 | * [#129](https://github.com/svrxjs/svrx/pull/129) Prevent modify package.json when plugin installation ([@xuchaoying](https://github.com/xuchaoying)) 88 | * `svrx` 89 | * [#132](https://github.com/svrxjs/svrx/pull/132) Fix process exit not working on Windows ([@xuchaoying](https://github.com/xuchaoying)) 90 | 91 | ## v1.0.4 (2019-10-18) 92 | 93 | #### :bug: Bug Fix 94 | * `svrx` 95 | * [#112](https://github.com/svrxjs/svrx/pull/112) Fix css is not appended if there's no tag ([@leeluolee](https://github.com/leeluolee)) 96 | * [#119](https://github.com/svrxjs/svrx/pull/119) Fix routing update not working ([@leeluolee](https://github.com/leeluolee)) 97 | * [#120](https://github.com/svrxjs/svrx/pull/120) Fix scripts injecting bug ([@leeluolee](https://github.com/leeluolee)) 98 | 99 | ## v1.0.3 (2019-10-11) 100 | 101 | #### :rocket: New Feature 102 | * `svrx-util`, `svrx` 103 | * [#110](https://github.com/svrxjs/svrx/pull/110) Add watch(), del(), splice() to config ([@xuchaoying](https://github.com/xuchaoying)) 104 | 105 | ## v1.0.0 (2019-09-20) 106 | 107 | #### :rocket: New Feature 108 | * `svrx` 109 | * [#90](https://github.com/svrxjs/svrx/pull/90) Add plublic events ([@leeluolee](https://github.com/leeluolee)) 110 | * [#86](https://github.com/svrxjs/svrx/pull/86) Alias registService to regist ([@leeluolee](https://github.com/leeluolee)) 111 | 112 | #### :bug: Bug Fix 113 | * `svrx` 114 | * [#84](https://github.com/svrxjs/svrx/pull/84) Fix dashed plugin name parse error ([@xuchaoying](https://github.com/xuchaoying)) 115 | * `svrx-util`, `svrx` 116 | * [#83](https://github.com/svrxjs/svrx/pull/83) Return the right package info after npmi ([@xuchaoying](https://github.com/xuchaoying)) 117 | 118 | ## 0.0.9 (2019-09-04) 119 | 120 | ## 0.0.8 (2019-09-04) 121 | 122 | #### :bug: Bug Fix 123 | * `svrx` 124 | * [#77](https://github.com/svrxjs/svrx/pull/77) Fix number check for option value ([@xuchaoying](https://github.com/xuchaoying)) 125 | 126 | ## v0.0.7 (2019-08-26) 127 | 128 | #### :boom: Breaking Change 129 | * `svrx-util`, `svrx` 130 | * [#66](https://github.com/svrxjs/svrx/pull/66) Set default value of proxy.changeOrigin to true ([@xuchaoying](https://github.com/xuchaoying)) 131 | 132 | #### :bug: Bug Fix 133 | * `svrx` 134 | * [#26](https://github.com/svrxjs/svrx/pull/26) Fix parse plugin querystring with dot string ([@xuchaoying](https://github.com/xuchaoying)) 135 | * [#27](https://github.com/svrxjs/svrx/pull/27) Fix router not work when historyfallback set to true ([@xuchaoying](https://github.com/xuchaoying)) 136 | 137 | #### :nail_care: Enhancement 138 | * `svrx-util`, `svrx` 139 | * [#28](https://github.com/svrxjs/svrx/pull/28) Enable multi-process asynchronous plugin installation ([@leeluolee](https://github.com/leeluolee)) 140 | * `svrx` 141 | * [#25](https://github.com/svrxjs/svrx/pull/25) Support relative path to open plugin ([@leeluolee](https://github.com/leeluolee)) 142 | 143 | ## v0.0.6 (2019-07-30) 144 | 145 | #### :bug: Bug Fix 146 | * `svrx` 147 | * [#22](https://github.com/svrxjs/svrx/pull/22) Fix config.get() to get all default values ([@xuchaoying](https://github.com/xuchaoying)) 148 | 149 | #### :nail_care: Enhancement 150 | * `svrx` 151 | * [#23](https://github.com/svrxjs/svrx/pull/23) Add plugin version match check ([@xuchaoying](https://github.com/xuchaoying)) 152 | 153 | ## v0.0.5 (2019-07-25) 154 | 155 | #### :bug: Bug Fix 156 | * `svrx` 157 | * [#17](https://github.com/svrxjs/svrx/pull/17) Set charset for injected script ([@xuchaoying](https://github.com/xuchaoying)) 158 | * [#19](https://github.com/svrxjs/svrx/pull/19) Fix https not working ([@leeluolee](https://github.com/leeluolee)) 159 | 160 | ## v0.0.4 (2019-07-16) 161 | 162 | #### :rocket: New Feature 163 | * `svrx` 164 | * [#12](https://github.com/svrxjs/svrx/pull/12) Add proxy action ([@xuchaoying](https://github.com/xuchaoying)) 165 | * [#11](https://github.com/svrxjs/svrx/pull/11) Add --plugin to define plugin and options ([@xuchaoying](https://github.com/xuchaoying)) 166 | 167 | #### :nail_care: Enhancement 168 | * `svrx` 169 | * [#9](https://github.com/svrxjs/svrx/pull/9) Add global rc config ([@xuchaoying](https://github.com/xuchaoying)) 170 | * `svrx` 171 | * [#8](https://github.com/svrxjs/svrx/pull/8) Enhance help print ([@xuchaoying](https://github.com/xuchaoying)) 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 NetEase, Inc. and its affiliates. 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | svrx 8 | 9 | 10 | node 11 | 12 | 13 | Build Status 14 | 15 | 16 | codecov 17 | 18 | 19 | Dependencies 20 | 21 | 22 | DevDependencies 23 | 24 | 25 | gitter 26 | 27 |

28 | 29 | English | [中文](README.zh-CN.md) 30 | 31 | > A pluggable frontend server, it just works 32 | 33 | Server-X(svrx) is a platform built for efficient front-end development. 34 | 35 | ## Motivation 36 | 37 | As a front-end developer, to meet different kind of development requirements, 38 | usually we will have one or more set of fixed development environment, 39 | in which may include a local dev server and many other debug tools. 40 | **It's difficult to maintain a development environment**: 41 | you need to install and configure every tool separately. 42 | Besides, you may also need to enable or disable a tool when switching among projects. 43 | 44 | To solve the problem, we plan to **integrate all the development services and tools into a pluggable platform**, 45 | and name it **Server-X(svrx)**. 46 | With Server-X, you can **freely pick and combine any services(plugins) you want**, 47 | like static serve, proxy, remote debugging and etc, 48 | without concerning about plugin installation. 49 | 50 | Now, Server-X makes it possible for us to easily customize the development environment for each project, 51 | and **instead of downloading many other packages, all you need to do is just install Server-X**. 52 | 53 | ## Features 54 | 55 | 🍻 **Serve** a static site or SPA in current directory 56 | 🐱 Easy to **proxy** everything 57 | 🏈 **Auto refresh** the page on sources change(inline reload on stylesheets change) 58 | 🍀 **Powerful plugins**: use without installation 59 | 🐥 **Routing with hot reload**: never restart your server 60 | 🚀 **Toolkit** for quick custom plugin development 61 | 🎊 ... 62 | 63 | ![](https://svrx.io/assets/images/demo.gif) 64 | 65 | Here's an example showing how to start a devServer with Server-X, 66 | only with a simple command: 67 | 68 | ```bash 69 | svrx -p qrcode 70 | ``` 71 | 72 | After code change, just save the files to make sure livereload works. 73 | And here's also a tiny plugin named `qrcode` to display a qrcode of this page. 74 | Remember, you don't need to install any plugins, just declare it. 75 | 76 | ## Quick Start 77 | 78 | ### Install 79 | 80 | ```bash 81 | npm install -g @svrx/cli 82 | ``` 83 | 84 | ### Usage 85 | 86 | Before we start, you need to cd into the root of your project first. Let's say you've already got an `index.html` in your project: 87 | 88 | ```bash 89 | cd your_project 90 | ls # index.html 91 | ``` 92 | 93 | And without any other config, just run `svrx` command to start the dev server: 94 | 95 | ```bash 96 | svrx 97 | ``` 98 | 99 | Then visit http://localhost:8000 to see the content of index.html. 100 | 101 | ![](https://svrx.io/assets/demo.png) 102 | 103 | ### Command Line Options 104 | 105 | You can pass options to change the default behavior through command line: 106 | 107 | ```bash 108 | svrx --port 3000 --https --no-livereload 109 | ``` 110 | 111 | Check out the full option reference doc [here](https://docs.svrx.io/en/guide/option.html). 112 | 113 | ### .svrxrc.js 114 | 115 | And also, you can write down all your options by creating a file named `.svrxrc.js` or `svrx.config.js` in the root path of your project. 116 | 117 | ```javascript 118 | // .svrxrc.js 119 | module.exports = { 120 | port: 3000, 121 | https: true, 122 | livereload: false 123 | }; 124 | ``` 125 | 126 | And then run `svrx` command, svrx will read your options from the config file automatically. 127 | 128 | ## Feature - Plugins 129 | 130 | Again, you don't need to install any plugins, just use it! 131 | Server-X will handle everything(such as install, update...) for you. 132 | 133 | You can use plugins through command line options, eg: 134 | 135 | ```bash 136 | svrx --plugin markdown -p qrcode # -p is alias of --plugin 137 | svrx --markdown --qrcode # set a pluginName to true to start a plugin quickly 138 | ``` 139 | 140 | And also, you can enable and config a plugin through plugins in `.svrxrc.js` file, eg: 141 | 142 | ```javascript 143 | // .svrxrc.js 144 | module.exports = { 145 | plugins: [ 146 | 'markdown', 147 | { 148 | name: 'qrcode', 149 | options: { 150 | ui: false, 151 | }, 152 | }, 153 | ], 154 | }; 155 | ``` 156 | 157 | [👉 See all plugins](https://svrx.io/plugin?query=svrx-plugin-) 158 | 159 | ### Write Your Own Plugin 160 | 161 | If, unluckily, you didn't find a proper plugin you need, 162 | you can try write one with our [plugin-dev-tool](https://github.com/svrxjs/svrx-create-plugin) ! 163 | As a pure plugin platform, Server-X encapsulates a lot of basic logic for you, 164 | which makes it rather easy to write a new plugin. 165 | By the way, in general, you can easily write a plugin with code **less than 50 lines**, 166 | just like most of our published plugins. 167 | 168 | So what can we do through the plugins? We can: 169 | 170 | - Inject code, script, styles into the front-end page 171 | - eg: [vConsole plugin](https://github.com/svrxjs/svrx-plugin-vconsole) 、[qrcode plugin](https://github.com/svrxjs/svrx-plugin-qrcode) 172 | - Intercept the backend requests, edit and proxy those data 173 | - eg: [Mock.js plugin](https://github.com/svrxjs/svrx-plugin-mock) 、 [JSON-Server plugin](https://github.com/svrxjs/svrx-plugin-json-server) 174 | 175 | Anyway, Server-X provides a powerful ability to inject both frontend and backend logic, 176 | all you need to do is use it to create your own magic plugins. 177 | 178 | You can read more about plugin development [here](https://docs.svrx.io/en/plugin/contribution.html) . 179 | 180 | ## Feature - Routing 181 | 182 | You can try the following commands to start Server-X routing quickly: 183 | 184 | ```bash 185 | touch route.js # create empty routing file 186 | svrx --route route.js 187 | ``` 188 | 189 | In your `route.js` 190 | 191 | ``` 192 | get('/blog').to.json({ title: 'svrx' }); 193 | ``` 194 | 195 | Then open `/blog`, you'll see the json output `{title: 'svrx'}`. 196 | 197 | Features of routing: 198 | - support hot reloading ( check it out by editing your route.js now) 199 | - easy writing, clear reading 200 | - support [expanding](https://docs.svrx.io/en/guide/route.html#plugin) through plugin 201 | 202 | Besides return of json, you can also: 203 | 204 | ``` 205 | get('/handle(.*)').to.handle((ctx) => { ctx.body = 'handle'; }); 206 | get('/html(.*)').to.send('haha'); 207 | get('/rewrite:path(.*)').to.rewrite('/query{path}'); 208 | get('/redirect:path(.*)').to.redirect('localhost:9002/proxy{path}'); 209 | get('/api(.*)').to.proxy('http://mock.server.com/') 210 | ... 211 | ``` 212 | 213 | To learn more about the grammar and usage of Routing, click [here](https://docs.svrx.io/en/guide/route.html). 214 | 215 | ## Documentation 216 | 217 | You can read more detail about the usage, API reference, blogs [here](https://docs.svrx.io/en/). 218 | 219 | ## Support 220 | 221 | Feel free to [raise an issue](https://github.com/svrxjs/svrx/issues/new/choose). 222 | 223 | ## Contributing 224 | 225 | Please see the [contributing guidelines](https://docs.svrx.io/en/contribution.html). 226 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | svrx 8 | 9 | 10 | node 11 | 12 | 13 | Build Status 14 | 15 | 16 | codecov 17 | 18 | 19 | Dependencies 20 | 21 | 22 | DevDependencies 23 | 24 | 25 | gitter 26 | 27 |

28 | 29 | 中文 | [English](README.md) 30 | 31 | > A pluggable frontend server, it just works 32 | 33 | Server-X(svrx) 是一个渐进且易于使用的、插件化的前端开发工作台。 34 | 35 | ## Motivation 36 | 37 | 作为前端开发,在不同的开发需求下,一般来说我们会有一套或者多套固定的开发环境。 38 | 它可能包括本地服务器以及各种用于调试工程的小工具。 39 | **维护这样的开发环境是很麻烦的**: 40 | 你不仅需要单独安装每一个工具,还需要对每一个工具进行设置。 41 | 此外,针对不同的工程,你还需要有选择地去开启或关闭某个功能。 42 | 43 | Server-X 做的,就是**利用插件机制来整合各种前端开发服务**, 44 | 让前端开发者可以自由挑选所需的功能,如静态伺服、代理、远程调试等, 45 | 且**无需关心这些功能插件的安装过程**。 46 | 有了 Server-X 这样一个轻量的前端开发工作台, 47 | 我们可以**轻松做到一份配置对应一套开发环境,实现真正的一键启动开发服务**。 48 | 49 | ## Features 50 | 51 | 🍻 在当前页 **静态伺服** 静态文件或者一个 SPA 52 | 🐱 轻松实现 **代理转发** 53 | 🏈 资源更改 **自动重载页面** 54 | 🍀 **强大的插件机制**: 直接使用,无需安装 55 | 🐥 **支持热重载的路由**: 永远不需要重启服务器 56 | 🚀 **开发者套件** 快速开发自己的插件 57 | 🎊 ... 58 | 59 | ![](https://svrx.io/assets/images/demo.gif) 60 | 61 | 这是一个简单的使用 Server-X 开启本地服务的例子,只需要一行命令: 62 | 63 | ```bash 64 | svrx -p qrcode 65 | ``` 66 | 67 | 每次代码改动后,只需要 `ctrl+s` 页面就会自动刷新(css 改动时页面支持 hotReload )。 68 | 并且这里还使用了一个小插件叫做 `qrcode`,它会在页面右上角展示一个页面的二维码,方便移动端开发。 69 | 注意,你不需要自己安装任何插件,只需要声明它即可。 Server-X 会为你搞定一切! 70 | 71 | ## Quick Start 72 | 73 | ### 安装 74 | 75 | ```bash 76 | npm install -g @svrx/cli 77 | ``` 78 | 79 | ### 使用 80 | 81 | 开始前,首先你需要进入到你的工程目录,我们假设你的工程中已经有一个 `index.html`: 82 | 83 | ```bash 84 | cd your_project 85 | ls # index.html 86 | ``` 87 | 88 | 无需经过任何配置和传参,直接运行 `svrx` 命令即可开启一个简单的本地服务器: 89 | 90 | ```bash 91 | svrx 92 | ``` 93 | 94 | 此时访问 http://localhost:8000 ,就可以看到 `index.html` 中的内容了。 95 | 96 | ![](https://svrx.io/assets/demo.png) 97 | 98 | ### 使用参数 99 | 100 | 如果需要对 `svrx` 进行配置,可以通过命令行传参来实现: 101 | 102 | ```bash 103 | svrx --port 3000 --https --no-livereload 104 | ``` 105 | 106 | 详细的参数列表可以在 [这里](https://docs.svrx.io/zh/guide/option.html) 查看。 107 | 108 | ### 配置持久化 109 | 110 | 当然,你也可以在你的工程目录下建立 `.svrxrc.js` 或 `svrx.config.js` 配置文件,将上面的命令行参数持久化下来: 111 | 112 | ```javascript 113 | // .svrxrc.js 114 | module.exports = { 115 | port: 3000, 116 | https: true, 117 | livereload: false 118 | }; 119 | ``` 120 | 121 | 然后直接运行 `svrx` 命令, svrx 会自动读取你的配置文件。 122 | 123 | ## 核心功能 - 插件 124 | 125 | 再次声明,你不需要安装任何插件,直接使用即可! 126 | Server-X 会帮你自动处理插件的安装、更新等等流程。 127 | 128 | 你可以通过命令行的方式去使用插件,例如: 129 | 130 | ```bash 131 | svrx --plugin markdown -p qrcode # -p 是 --plugin 的缩写 132 | svrx --markdown --qrcode # 在命令行中设置某个插件名为 true 也可以快速开启一个插件 133 | ``` 134 | 135 | 同样的,你也可以通过配置 `.svrxrc.js` 中的 plugins 字段来启用或配置插件,如: 136 | 137 | ```javascript 138 | // .svrxrc.js 139 | module.exports = { 140 | plugins: [ 141 | 'markdown', 142 | { 143 | name: 'qrcode', 144 | options: { 145 | ui: false, 146 | }, 147 | }, 148 | ], 149 | }; 150 | ``` 151 | 152 | [👉 查看全部插件](https://svrx.io/plugin?query=svrx-plugin-) 153 | 154 | ### 定制你的插件 155 | 156 | 如果很不幸,你暂时没有找到合适的满足你需求的 Server-X 插件, 157 | 你可以尝试使用我们的 [插件开发工具](https://github.com/svrxjs/svrx-create-plugin) 自己写一个小插件! 158 | Server-X 作为一个纯粹的插件平台,帮你封装了绝大多数底层逻辑,你的插件编写将会变得非常容易。 159 | 像上面列表中的绝大多数插件,**核心代码都没有超过50行**! 160 | 161 | 那么 Server-X 的插件可以实现些什么呢? 你可以: 162 | 163 | - 往前端页面注入脚本代码、样式代码等等 164 | - eg: [vConsole 插件](https://github.com/svrxjs/svrx-plugin-vconsole) 、[qrcode 插件](https://github.com/svrxjs/svrx-plugin-qrcode) 165 | - 拦截后端请求,对数据进行编辑、转发 166 | - eg: [Mock.js 插件](https://github.com/svrxjs/svrx-plugin-mock) 、 [JSON-Server 插件](https://github.com/svrxjs/svrx-plugin-json-server) 167 | 168 | 总之,Server-X 为你提供了强大的前后端代码注入能力,剩下的就靠你的创造力了。 169 | 170 | 关于插件的详细开发指南请阅读 [插件开发](https://docs.svrx.io/zh/plugin/contribution.html) 。 171 | 172 | ## 核心功能 - 动态路由 173 | 174 | 你可通过以下命令来快速尝试 Server-X 的动态路由功能。 175 | 176 | ```bash 177 | touch route.js # create empty routing file 178 | svrx --route route.js 179 | ``` 180 | 181 | 在 `route.js` 中 182 | 183 | ``` 184 | get('/blog').to.json({ title: 'svrx' }); 185 | ``` 186 | 187 | 打开 `/blog`,你将看到 `{title: 'svrx'}` 的 json 输出。 188 | 189 | 动态路由功能具有以下特性: 190 | - 支持 hot reloading ( 通过编辑 route.js 来验证 ) 191 | - 简单的书写,直观的阅读 192 | - 支持通过插件来[扩展和分发](https://docs.svrx.io/zh/guide/route.html#plugin) 193 | 194 | 除了返回 json,你还可以: 195 | 196 | ``` 197 | get('/handle(.*)').to.handle((ctx) => { ctx.body = 'handle'; }); 198 | get('/html(.*)').to.send('haha'); 199 | get('/rewrite:path(.*)').to.rewrite('/query{path}'); 200 | get('/redirect:path(.*)').to.redirect('localhost:9002/proxy{path}'); 201 | get('/api(.*)').to.proxy('http://mock.server.com/') 202 | ... 203 | ``` 204 | 205 | 关于动态路由的语法和使用,可以在 [这里](https://docs.svrx.io/zh/guide/route.html) 找到更详细的说明。 206 | 207 | ## Documentation 208 | 209 | 你可以在 [这里](https://docs.svrx.io/zh/) 阅读更详细的使用文档、API 列表以及我们的博客。 210 | 211 | ## Support 212 | 213 | 如果你有任何问题、建议、bug,欢迎给我们 [提 Issue](https://github.com/svrxjs/svrx/issues/new/choose) 。 214 | 215 | ## Contributing 216 | 217 | 请阅读 [贡献指南](https://docs.svrx.io/zh/contribution.html) 。 218 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 90 6 | threshold: 0.01% 7 | base: auto 8 | patch: 9 | default: 10 | target: 90 11 | threshold: 0.01% 12 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /examples/eruda/README.md: -------------------------------------------------------------------------------- 1 | # svrx-plugin-eruda 2 | 3 | The eruda plugin for svrx 4 | 5 | ```bash 6 | svrx --eruda 7 | ``` 8 | 9 | visit the http://localhost:8000. The eruda 's icon will show up on every page you visited 10 | 11 | ![](https://user-images.githubusercontent.com/24988831/60713045-df0bce80-9f4a-11e9-801d-1fb4dd79224a.png) 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/portal/README.md: -------------------------------------------------------------------------------- 1 | # svrx-plugin-portal 2 | 3 | The portal plugin for svrx. Provide intranet penetration 4 | 5 | ```bash 6 | svrx --portal 7 | ``` 8 | 9 | visit the url shown in log 10 | 11 | ![](https://user-images.githubusercontent.com/24988831/60712982-bd124c00-9f4a-11e9-8cf6-aae99772713e.png) -------------------------------------------------------------------------------- /examples/proxy/.svrxrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | serve: { 3 | base: './public', 4 | }, 5 | proxy: [ 6 | { 7 | context: ['/api', '/same'], 8 | target: 'https://randomuser.me', 9 | pathRewrite: { 10 | '/same/api': '/api' 11 | } 12 | } 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /examples/proxy/README.md: -------------------------------------------------------------------------------- 1 | # Set Proxy Rules 2 | 3 | Here is an example we porxy `/api` and `/same/api` to the target host `https://randomuser.me`. 4 | 5 | Note that we also rewrite `/same/api` to `/api`, so `/same/api` will be proxy to `https://randomuser.me/api` 6 | -------------------------------------------------------------------------------- /examples/proxy/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Set Proxy Rules 6 | 7 | 8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/proxy/public/main.js: -------------------------------------------------------------------------------- 1 | function onFetch(url) { 2 | fetch(url) 3 | .then((resp) => resp.json()) 4 | .then((result) => { 5 | console.log(result); // eslint-disable-line 6 | if (result && result.results) { 7 | const data = result.results[0]; 8 | const info = { 9 | name: `${data.name.first} ${data.name.last}`, 10 | email: data.email, 11 | }; 12 | const textarea = document.getElementById(url); 13 | textarea.value = Object.keys(info) 14 | .map((key) => `${key}: ${info[key]}`) 15 | .join('\n'); 16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /examples/serve-static-page/.svrxrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | serve: { 3 | base: './public', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/serve-static-page/README.md: -------------------------------------------------------------------------------- 1 | # Serve a static page 2 | 3 | To serve a static page, you just need to set `serve.base` to the static directory. 4 | -------------------------------------------------------------------------------- /examples/serve-static-page/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Your Static Page 6 | 7 | 8 |

hello world!

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/serve-static-page/public/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-alert */ 2 | alert('hello!'); 3 | -------------------------------------------------------------------------------- /examples/weinre/README.md: -------------------------------------------------------------------------------- 1 | # svrx-plugin-weinre 2 | 3 | The weinre plugin for svrx 4 | 5 | ```bash 6 | svrx --weinre 7 | ``` 8 | 9 | ![](https://user-images.githubusercontent.com/24988831/60713097-f6e35280-9f4a-11e9-9aa5-2796dcc7aae1.png) 10 | 11 | visit http://localhost:8001 12 | 13 | ![](https://user-images.githubusercontent.com/24988831/60713159-1ed2b600-9f4b-11e9-84cc-cd2444d9191c.png) 14 | 15 | ![](https://user-images.githubusercontent.com/24988831/60713196-2e51ff00-9f4b-11e9-91fe-5a147eee472d.png) 16 | 17 | 18 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.1.7", 6 | "npmClient": "npm", 7 | "changelog": { 8 | "repo": "svrxjs/svrx", 9 | "labels": { 10 | "PR: new feature": ":rocket: New Feature", 11 | "PR: breaking change": ":boom: Breaking Change", 12 | "PR: bug fix": ":bug: Bug Fix", 13 | "PR: enhancement": ":nail_care: Enhancement" 14 | }, 15 | "cacheDir": ".changelog" 16 | }, 17 | "command": { 18 | "version": { 19 | "message": "chore(release): publish %s" 20 | }, 21 | "publish": { 22 | "ignoreChanges": [ 23 | "ignored-file", 24 | "*.md", 25 | "*.txt", 26 | "__tests__/**" 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "build": "lerna run build", 6 | "lint": "run-p lint:*", 7 | "lint:js": "eslint \"**/*.js\"", 8 | "fix": "run-p fix:*", 9 | "fix:js": "eslint \"**/*.js\" --fix", 10 | "test": "lerna run test", 11 | "clean": "run-s clean:*", 12 | "clean:dependency": "lerna clean -y && rimraf -rf node_modules", 13 | "report-coverage": "lerna run report-coverage", 14 | "lerna:version": "lerna version", 15 | "lerna:publish": "lerna publish from-git --yes", 16 | "postinstall": "lerna exec -- npm install", 17 | "prepare": "npm run build", 18 | "prepublishOnly": "lerna exec -- npm prune --production && lerna exec -- npm shrinkwrap" 19 | }, 20 | "devDependencies": { 21 | "@commitlint/cli": "^8.2.0", 22 | "@commitlint/config-conventional": "^8.2.0", 23 | "@svrx/eslint-config": "^1.0.0", 24 | "codecov": "^3.6.1", 25 | "eslint": "^6.4.0", 26 | "expect.js": "^0.3.1", 27 | "husky": "^3.0.5", 28 | "lerna": "^3.19.0", 29 | "lerna-changelog": "^0.8.3", 30 | "lint-staged": "^9.3.0", 31 | "mocha": "^6.1.4", 32 | "npm-run-all": "^4.1.5", 33 | "nyc": "^14.1.0", 34 | "rimraf": "^3.0.0", 35 | "sinon": "^7.5.0", 36 | "supertest": "^4.0.2", 37 | "webpack": "^4.41.0", 38 | "webpack-cli": "^3.3.9" 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "pre-commit": "lint-staged", 43 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 44 | } 45 | }, 46 | "dependencies": { 47 | "@svrx/svrx": "file:packages/svrx", 48 | "@svrx/util": "file:packages/svrx-util" 49 | }, 50 | "lint-staged": { 51 | "*.js": [ 52 | "eslint --fix", 53 | "git add" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/svrx-util/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /packages/svrx-util/README.md: -------------------------------------------------------------------------------- 1 | # @svrx/util 2 | 3 | > General utilities for svrx. 4 | 5 | svrx(server-x) is a platform built for efficient front-end development. 6 | 7 | See our [website](https://svrx.io/) for more information. 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm install --save-dev @svrx/util 13 | ``` -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/config/.svrxrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | https: true, 3 | open: false, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/plugins/hello/0.0.5/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'hello', 3 | priority: 100, 4 | options: { 5 | limit: { 6 | type: 'number', 7 | default: 5, 8 | }, 9 | }, 10 | hooks: { 11 | async onRoute(ctx, next, { config }) { 12 | const limit = config.get('limit'); 13 | ctx.set('X-Svrx-Limit', limit); 14 | await next(); 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/plugins/hello/0.0.5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svrx-plugin-hello", 3 | "version": "0.0.5", 4 | "keywords": [ 5 | "svrx", 6 | "plugin" 7 | ], 8 | "license": "MIT", 9 | "main": "./index.js", 10 | "engines": { 11 | "svrx": "~0.0.1" 12 | }, 13 | "dependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/plugins/hello/1.0.0/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'hello', 3 | priority: 100, 4 | options: { 5 | limit: { 6 | type: 'number', 7 | default: 100, 8 | }, 9 | }, 10 | hooks: { 11 | async onRoute(ctx, next, { config }) { 12 | const limit = config.get('limit'); 13 | ctx.set('X-Svrx-Limit', limit); 14 | await next(); 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/plugins/hello/1.0.0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svrx-plugin-hello", 3 | "version": "1.0.0", 4 | "keywords": [ 5 | "svrx", 6 | "plugin" 7 | ], 8 | "license": "MIT", 9 | "main": "./index.js", 10 | "engines": { 11 | "svrx": "^1.0.0" 12 | }, 13 | "dependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/plugins/hello/1.0.1/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'hello', 3 | priority: 100, 4 | options: { 5 | limit: { 6 | type: 'number', 7 | default: 101, 8 | }, 9 | }, 10 | hooks: { 11 | async onRoute(ctx, next, { config }) { 12 | const limit = config.get('limit'); 13 | ctx.set('X-Svrx-Limit', limit); 14 | await next(); 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/plugins/hello/1.0.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svrx-plugin-hello", 3 | "version": "1.0.1", 4 | "keywords": [ 5 | "svrx", 6 | "plugin" 7 | ], 8 | "license": "MIT", 9 | "main": "./index.js", 10 | "engines": { 11 | "svrx": "^1.0.0" 12 | }, 13 | "dependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/versions/1.0.6/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svrxjs/svrx/7abfdc3331fbbf5d06d8fd04eb4f82d00e3c683e/packages/svrx-util/__tests__/fixture/.svrx/versions/1.0.6/index.js -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/.svrx/versions/1.0.6/lib/svrx.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svrxjs/svrx/7abfdc3331fbbf5d06d8fd04eb4f82d00e3c683e/packages/svrx-util/__tests__/fixture/.svrx/versions/1.0.6/lib/svrx.js -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/plugin/svrx-plugin-error-no-version/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'error-no-version', 3 | priority: 100, 4 | }; 5 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/plugin/svrx-plugin-error-no-version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svrx-plugin-error-no-version", 3 | "keywords": [ 4 | "svrx", 5 | "plugin" 6 | ], 7 | "license": "MIT", 8 | "main": "./index.js", 9 | "engines": { 10 | "svrx": "^0.0.1" 11 | }, 12 | "dependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/plugin/svrx-plugin-no-package/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'no-package', 3 | hooks: { 4 | async onRoute(ctx, next, { config }) { 5 | const limit = config.get('limit'); 6 | ctx.set('X-Svrx-Limit', limit); 7 | await next(); 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/plugin/svrx-plugin-test/assets/index.css: -------------------------------------------------------------------------------- 1 | body{background: black} -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/plugin/svrx-plugin-test/assets/index.js: -------------------------------------------------------------------------------- 1 | console.log('svrx-plugin-test') -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/plugin/svrx-plugin-test/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'test', 3 | priority: 100, 4 | options: { 5 | limit: { 6 | type: 'number', 7 | default: 100, 8 | }, 9 | }, 10 | assets: { 11 | style: ['./assets/index.css'], 12 | script: ['./assets/index.js'], 13 | }, 14 | hooks: { 15 | async onRoute(ctx, next, { config }) { 16 | const limit = config.get('limit'); 17 | ctx.set('X-Svrx-Limit', limit); 18 | await next(); 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/fixture/plugin/svrx-plugin-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svrx-plugin-test", 3 | "version": "0.0.1", 4 | "keywords": [ 5 | "svrx", 6 | "plugin" 7 | ], 8 | "license": "MIT", 9 | "main": "./index.js", 10 | "engines": { 11 | "svrx": "^0.0.1" 12 | }, 13 | "dependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/spec/logger.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const { Writable } = require('stream'); 3 | const logger = require('../../lib/logger'); 4 | 5 | const { Logger } = logger; 6 | 7 | describe('logger', () => { 8 | function log(msg, label) { 9 | return new Promise((resolve) => { 10 | let cached = ''; 11 | Logger.stream = new Writable({}); 12 | Logger.stream._write = (chunk, encode, cb) => { 13 | cached += chunk.toString(); 14 | cb(); 15 | }; 16 | 17 | const l = new logger.Logger(); 18 | 19 | Logger.stream.on('finish', () => { 20 | Logger.stream = process.stdout; 21 | resolve(cached); 22 | }); 23 | l[label](msg); 24 | Logger.stream.end(); 25 | }); 26 | } 27 | 28 | it('log function', (done) => { 29 | Logger.setLevel('debug'); 30 | log('hello world', 'notify') 31 | .then((content) => { 32 | expect(content).to.match(/\[svrx\]/); 33 | return log('hello world', 'error'); 34 | }) 35 | .then((content) => { 36 | expect(content).to.match(/\[error\]/); 37 | return log('hello world', 'debug'); 38 | }) 39 | .then((content) => { 40 | expect(content).to.match(/\[debug\]/); 41 | return log('hello world', 'info'); 42 | }) 43 | .then((content) => { 44 | expect(content).to.match(/\[info\]/); 45 | return log('hello world', 'warn'); 46 | }) 47 | .then((content) => { 48 | expect(content).to.match(/\[warn\]/); 49 | Logger.setLevel('error'); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('log level', (done) => { 55 | Logger.setLevel('error'); 56 | log('hello world', 'notify') 57 | .then((content) => { 58 | expect(content).to.match(/\[svrx\]/); 59 | return log('hello world', 'error'); 60 | }) 61 | .then((content) => { 62 | expect(content).to.match(/\[error\]/); 63 | return log('hello world', 'debug'); 64 | }) 65 | .then((content) => { 66 | expect(content).to.equal(''); 67 | return log('hello world', 'info'); 68 | }) 69 | .then((content) => { 70 | expect(content).to.equal(''); 71 | return log('hello world', 'warn'); 72 | }) 73 | .then((content) => { 74 | expect(content).to.equal(''); 75 | Logger.setLevel('error'); 76 | done(); 77 | }); 78 | }); 79 | 80 | it('Logger.lock', (done) => { 81 | Logger.lock(); 82 | 83 | log('hello world', 'notify').then((content) => { 84 | expect(content).to.equal(''); 85 | Logger.release(); 86 | log('hello world', 'notify').then((cnt) => { 87 | expect(cnt).to.match(/hello world/); 88 | done(); 89 | }); 90 | }); 91 | }); 92 | 93 | it('should change category throught #getPluginLogger()', () => { 94 | const pluginLogger = logger.getPluginLogger('test-plugin'); 95 | expect(pluginLogger.category).to.eql('test-plugin'); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/spec/name-formatter.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const { normalizePluginName, parsePluginName } = require('../../lib/name-formatter'); 3 | 4 | describe('Name Formatter', () => { 5 | const plugins = [ 6 | { name: 'svrx-plugin-foo', pluginName: 'foo' }, 7 | { name: 'svrx-plugin-foo-bar', pluginName: 'foo-bar' }, 8 | { name: '@scope/svrx-plugin-foo', pluginName: '@scope/foo' }, 9 | { name: '@scope/svrx-plugin-foo-bar', pluginName: '@scope/foo-bar' }, 10 | ]; 11 | 12 | it('normalizePluginName', () => { 13 | plugins.forEach((p) => { 14 | expect(normalizePluginName(p.pluginName)).to.equal(p.name); 15 | }); 16 | expect(normalizePluginName('@scope')).to.equal(null); 17 | expect(normalizePluginName('@scope/')).to.equal(null); 18 | expect(normalizePluginName('@SCOPE/bar')).to.equal(null); 19 | }); 20 | it('parsePluginName', () => { 21 | plugins.forEach((p) => { 22 | expect(parsePluginName(p.name)).to.equal(p.pluginName); 23 | }); 24 | expect(parsePluginName('svrx-not-a-legal-plugin')).to.equal(null); 25 | expect(parsePluginName('@scope/svrx-not-a-legal-plugin')).to.equal(null); 26 | expect(parsePluginName('@no_')).to.equal(null); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/spec/rc-file-read.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const libPath = require('path'); 3 | const rcFileRead = require('../../lib/rc-file-read'); 4 | 5 | const TEST_SVRX_DIR = libPath.join(__dirname, '../fixture/.svrx'); 6 | 7 | describe('rcFileRead', () => { 8 | const { SVRX_DIR } = process.env; 9 | before(() => { 10 | process.env.SVRX_DIR = TEST_SVRX_DIR; 11 | }); 12 | after(() => { 13 | process.env.SVRX_DIR = SVRX_DIR; 14 | }); 15 | 16 | it('should return global rc configs correctly', () => { 17 | const options = rcFileRead(); 18 | expect(options).to.eql({ 19 | https: true, 20 | open: false, 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/svrx-util/__tests__/svrx-util.test.js: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // const svrxUtil = require('..'); 4 | // 5 | // describe('svrx-util', () => { 6 | // it('needs tests'); 7 | // }); 8 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/c2k.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | /** 3 | * 4 | License 5 | MIT 6 | https://github.com/cyrilluce/koa2-connect#readme 7 | 8 | ## Fork chain 9 | - Copyright (c) 2014-present Vladimir Kurchatkin 10 | - Copyright (c) 2017-present cyrilluce 11 | - Copyright (c) 2019-present leeluolee 12 | */ 13 | 14 | /** 15 | * The middleware function does include the `next` callback so only resolve 16 | * the Promise when it's called. If it's never called, the middleware stack 17 | * completion will stall 18 | */ 19 | function handler(ctx, connectMiddleware, options) { 20 | options = options || {}; 21 | 22 | return new Promise((resolve, reject) => { 23 | function makeInjectedResponse(koaCtx, whenEnded) { 24 | const { res } = koaCtx; 25 | 26 | res.on('close', whenEnded).on('finish', whenEnded); 27 | 28 | // koa2.0 initial assign statusCode to 404, default reset it to 200 29 | let dummyRes; 30 | let statusCodeSetted = false; 31 | function default404to200() { 32 | if (!statusCodeSetted && res.statusCode === 404) { 33 | res.statusCode = 200; 34 | } 35 | } 36 | if (!dummyRes) { 37 | let buffer = Buffer.from([]); 38 | dummyRes = { 39 | __proto__: res, 40 | end(...args) { 41 | const cnt = args[0]; 42 | if (options.bubble) { 43 | ctx.respond = true; 44 | ctx.body = cnt ? Buffer.concat([buffer, Buffer.from(cnt)]) : buffer; 45 | resolve(false); // can't trigger finish or end 46 | } else { 47 | res.end(...args); 48 | } 49 | default404to200(); 50 | }, 51 | write(...args) { 52 | const cnt = args[0]; 53 | if (options.bubble) { 54 | ctx.respond = true; 55 | buffer = Buffer.concat([buffer, Buffer.from(cnt)]); 56 | } else { 57 | res.write(...args); 58 | } 59 | default404to200(); 60 | }, 61 | set statusCode(v) { 62 | statusCodeSetted = true; 63 | res.statusCode = v; 64 | }, 65 | get statusCode() { 66 | return res.statusCode; 67 | }, 68 | writeHead(...args) { 69 | statusCodeSetted = true; 70 | return res.writeHead(...args); 71 | }, 72 | setHeader(...args) { 73 | statusCodeSetted = true; 74 | return res.setHeader(...args); 75 | }, 76 | }; 77 | } 78 | 79 | return dummyRes; 80 | } 81 | // (req, res) 82 | const args = [ 83 | ctx.req, 84 | makeInjectedResponse(ctx, () => { 85 | resolve(false); 86 | }), 87 | ]; 88 | let assumeSync = true; 89 | // (req, res, next) or (err, req, res, next) 90 | if (connectMiddleware.length >= 3) { 91 | args.push((err) => { 92 | if (err) reject(err); 93 | else resolve(true); 94 | }); 95 | assumeSync = false; 96 | } 97 | // (err, req, res, next) 98 | if (connectMiddleware.length >= 4) { 99 | args.unshift(null); 100 | } 101 | connectMiddleware(...args); 102 | /** 103 | * If the middleware function does not declare receiving the `next` callback 104 | * assume that it's synchronous. 105 | */ 106 | if (assumeSync) { 107 | resolve(true); 108 | } 109 | }); 110 | } 111 | 112 | /** 113 | * Returns a Koa middleware function that varies its async logic based on if the 114 | * given middleware function declares at least 3 parameters, i.e. includes 115 | * the `next` callback function 116 | */ 117 | function koaConnect(connectMiddleware, options) { 118 | return async (ctx, next) => { 119 | ctx.respond = false; 120 | try { 121 | const goNext = await handler(ctx, connectMiddleware, options); 122 | 123 | if (goNext) { 124 | ctx.respond = true; 125 | return next(); 126 | } 127 | } catch (err) { 128 | ctx.respond = true; 129 | throw err; 130 | } 131 | return next(); 132 | }; 133 | } 134 | 135 | module.exports = koaConnect; 136 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/logger.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const ora = require('ora'); 3 | 4 | const LABEL_CONFIG = { 5 | silent: { 6 | index: 101, 7 | }, 8 | notify: { 9 | index: 100, 10 | text: 'svrx', 11 | color: 'Blue', 12 | }, 13 | error: { 14 | color: 'Red', 15 | index: 10, 16 | }, 17 | warn: { 18 | color: 'Yellow', 19 | index: 5, 20 | }, 21 | info: { 22 | color: 'Green', 23 | index: 3, 24 | }, 25 | debug: { 26 | color: 'Black', 27 | index: 1, 28 | }, 29 | }; 30 | 31 | const LEVELS = ['notify', 'error', 'warn', 'info', 'debug']; 32 | 33 | const STATE = { 34 | LOCKED: Symbol('LOCKED'), 35 | UNLOCKED: Symbol('UNLOCKED'), 36 | }; 37 | 38 | class Logger { 39 | static setLevel(level) { 40 | if (level in LABEL_CONFIG) { 41 | Logger.levelIndex = LABEL_CONFIG[level].index; 42 | } 43 | } 44 | 45 | static lock() { 46 | if (Logger.state === STATE.LOCKED) return; 47 | const stdout = Logger.stream; 48 | Logger.oldWrite = stdout._write; 49 | 50 | stdout._write = function write(...args) { 51 | args[0] = ''; 52 | return Logger.oldWrite.apply(this, args); 53 | }; 54 | 55 | Logger.state = STATE.LOCKED; 56 | } 57 | 58 | static release() { 59 | if (Logger.state !== STATE.LOCKED) return; 60 | Logger.stream._write = Logger.oldWrite; 61 | delete Logger.oldWrite; 62 | Logger.state = STATE.UNLOCKED; 63 | } 64 | 65 | constructor(category = 'global') { 66 | this.category = category; 67 | this.chalk = chalk; 68 | } 69 | 70 | _getWriteMsg(msg, label) { 71 | label = label || 'notify'; 72 | 73 | if (LEVELS.indexOf(label) === -1) { 74 | throw Error(`logger.${label}() isn't exsits`); 75 | } 76 | 77 | const { index } = LABEL_CONFIG[label]; 78 | 79 | const text = LABEL_CONFIG[label].text || label; 80 | 81 | if (index < Logger.levelIndex) return ''; 82 | 83 | const { category } = this; 84 | const { color } = LABEL_CONFIG[label]; 85 | const foreColor = color === 'White' ? 'gray' : 'white'; 86 | const bgColor = `bg${color}`; 87 | const padText = `[${text}${category === 'global' ? '' : `:${category}`}]`; 88 | const labelText = color ? chalk[foreColor][bgColor](padText) : padText; 89 | 90 | return `${labelText} ${msg}\n`; 91 | } 92 | 93 | write(msg, label, options) { 94 | options = options || {}; 95 | if (Logger.state === STATE.LOCKED && options.force !== true) { 96 | this.write('Logger is locked, some progress task need release', 'warn', { force: true }); 97 | return; 98 | } 99 | 100 | const content = this._getWriteMsg(msg, label); 101 | 102 | if (content) Logger.stream.write(content); 103 | } 104 | 105 | spin(msg, label) { 106 | const spinner = ora(this._getWriteMsg(msg, label)).start(); 107 | return () => { 108 | spinner.stop(); 109 | }; 110 | } 111 | 112 | progress(msg, label) { 113 | const releaseSpin = this.spin(msg, label); 114 | 115 | Logger.lock(); 116 | return function release() { 117 | Logger.release(); 118 | releaseSpin(); 119 | }; 120 | } 121 | 122 | log(...args) { 123 | return this.notify(...args); 124 | } 125 | } 126 | 127 | Logger.levelIndex = LABEL_CONFIG.warn.index; 128 | Logger.state = STATE.UNLOCKED; 129 | Logger.stream = process.stdout; 130 | 131 | LEVELS.forEach((level) => { 132 | Logger.prototype[level] = function write(msg) { 133 | this.write(msg, level); 134 | }; 135 | }); 136 | 137 | function getPluginLogger(name) { 138 | return new Logger(name); 139 | } 140 | 141 | const logger = new Logger(); 142 | 143 | logger.setLevel = Logger.setLevel; 144 | logger.Logger = Logger; 145 | logger.getPluginLogger = getPluginLogger; 146 | 147 | module.exports = logger; 148 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/name-formatter.js: -------------------------------------------------------------------------------- 1 | const PLUGIN_PREFIX = 'svrx-plugin-'; 2 | const scopeAndNameRegex = /^@([a-z\d][\w-.]+)\/([a-z\d][\w-.]*)/; 3 | 4 | /** 5 | * combine pluginName to packageName 6 | * - foo -> svrx-plugin-foo 7 | * - foo-bar -> svrx-plugin-foo-bar 8 | * - @scope/foo -> @scope/svrx-plugin-foo 9 | * - @scope/foo-bar -> @scope/svrx-plugin-foo-bar 10 | * @param name 11 | * @returns {string|null|*} 12 | */ 13 | const normalizePluginName = (name) => { 14 | const combineName = (n) => (n.startsWith(PLUGIN_PREFIX) ? n : PLUGIN_PREFIX + n); 15 | const isScoped = name.startsWith('@'); 16 | 17 | if (isScoped) { 18 | const matches = scopeAndNameRegex.exec(name); 19 | if (matches) { 20 | const scope = matches[1]; 21 | const realName = matches[2]; 22 | return `@${scope}/${combineName(realName)}`; 23 | } 24 | return null; 25 | } 26 | return combineName(name); 27 | }; 28 | 29 | /** 30 | * parse packageName to pluginName (revert normalizePluginName()) 31 | * @param packageName 32 | * @returns {null|*} 33 | */ 34 | const parsePluginName = (packageName) => { 35 | const isScoped = packageName.startsWith('@'); 36 | const removePrefix = (n) => (n.startsWith(PLUGIN_PREFIX) ? n.slice(PLUGIN_PREFIX.length) : null); 37 | 38 | if (isScoped) { 39 | const matches = scopeAndNameRegex.exec(packageName); 40 | if (matches) { 41 | const scope = matches[1]; 42 | const realName = matches[2]; 43 | const formattedName = removePrefix(realName); 44 | return formattedName ? `@${scope}/${formattedName}` : null; 45 | } 46 | return null; 47 | } 48 | return removePrefix(packageName); 49 | }; 50 | 51 | module.exports = { 52 | normalizePluginName, 53 | parsePluginName, 54 | }; 55 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/npCall/index.js: -------------------------------------------------------------------------------- 1 | // return promise by callback node-callback-style handler 2 | function npCall(callback, args, ctx) { 3 | args = args || []; 4 | 5 | return new Promise((resolve, reject) => { 6 | args.push((err, ret) => { 7 | if (err) return reject(err); 8 | return resolve(ret); 9 | }); 10 | 11 | callback.apply(ctx, args); 12 | }); 13 | } 14 | 15 | module.exports = npCall; 16 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/package-manager/index.js: -------------------------------------------------------------------------------- 1 | const PluginPackageManager = require('./plugin-package-manager'); 2 | const PackageManager = require('./package-manager'); 3 | 4 | /** 5 | * @example 6 | const pm = PackageManagerCreator({ 7 | plugin: 'lodash', // will install svrx core if missing 8 | path: undefined, // if install from path 9 | version: '1.0.0', 10 | coreVersion: '1.0.1', // version of current svrx core 11 | autoClean: true, // auto remove old packages 12 | }); 13 | const pkg = await pm.load(); 14 | */ 15 | const PackageManagerCreator = ({ 16 | plugin, 17 | ...options 18 | } = {}) => (plugin 19 | ? new PluginPackageManager({ 20 | name: plugin, // plugin name: foo, foo-bar, @scope/foo 21 | ...options, 22 | }) 23 | : new PackageManager({ 24 | name: 'svrx', 25 | ...options, 26 | }) 27 | ); 28 | 29 | module.exports = PackageManagerCreator; 30 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/package-manager/plugin-package-manager.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | const PackageManager = require('./package-manager'); 3 | const { normalizePluginName } = require('../name-formatter'); 4 | 5 | class PluginPackageManager extends PackageManager { 6 | constructor(options) { 7 | super(options); 8 | this.packageName = normalizePluginName(this.name); 9 | } 10 | 11 | getRoot() { 12 | return this.PLUGIN_ROOT; 13 | } 14 | 15 | versionMatch(pkg) { 16 | const { coreVersion } = this; 17 | return semver.satisfies(coreVersion, pkg.pattern); 18 | } 19 | } 20 | 21 | module.exports = PluginPackageManager; 22 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/rc-file-read.js: -------------------------------------------------------------------------------- 1 | const { cosmiconfigSync } = require('cosmiconfig'); 2 | const userHome = require('os').homedir(); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const RC_FILES = ['.svrxrc.js', 'svrx.config.js']; 7 | const readGlobal = () => { 8 | const configRoot = process.env.SVRX_DIR; 9 | if (!configRoot && !userHome) { 10 | return {}; 11 | } 12 | 13 | const root = configRoot || path.resolve(userHome, '.svrx'); 14 | const fileName = RC_FILES.find((file) => fs.existsSync(path.join(root, 'config', file))); 15 | 16 | if (fileName) { 17 | return require(path.join(root, 'config', fileName)); // eslint-disable-line 18 | } 19 | 20 | return {}; 21 | }; 22 | 23 | const readScope = () => { 24 | const explorer = cosmiconfigSync('svrx', { 25 | searchPlaces: RC_FILES, 26 | }); 27 | const result = explorer.search(); 28 | if (result && !result.isEmpty) { 29 | return result.config; 30 | } 31 | return {}; 32 | }; 33 | 34 | module.exports = () => { 35 | const globalConfig = readGlobal(); 36 | const scopeConfig = readScope(); 37 | 38 | return { ...globalConfig, ...scopeConfig }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/svrx-util/lib/svrx-util.js: -------------------------------------------------------------------------------- 1 | exports.PackageManagerCreator = require('./package-manager'); 2 | exports.nameFormatter = require('./name-formatter'); 3 | exports.rcFileRead = require('./rc-file-read'); 4 | exports.logger = require('./logger'); 5 | exports.c2k = require('./c2k'); 6 | -------------------------------------------------------------------------------- /packages/svrx-util/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@svrx/util", 3 | "version": "1.1.7", 4 | "description": "util package of svrx", 5 | "homepage": "https://svrx.io/", 6 | "license": "MIT", 7 | "main": "lib/svrx-util.js", 8 | "directories": { 9 | "lib": "lib" 10 | }, 11 | "files": [ 12 | "lib", 13 | "npm-shrinkwrap.json" 14 | ], 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "scripts": { 19 | "test": "nyc --reporter=html --reporter=text mocha --recursive __tests__/spec --exit", 20 | "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/svrxjs/svrx.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/svrxjs/svrx/issues" 28 | }, 29 | "dependencies": { 30 | "chalk": "^3.0.0", 31 | "cosmiconfig": "^6.0.0", 32 | "fs-extra": "^8.1.0", 33 | "lodash": "^4.17.11", 34 | "mkdirp": "^0.5.1", 35 | "ora": "^4.0.1", 36 | "rimraf": "^3.0.0", 37 | "semver": "^7.1.1", 38 | "tmp": "^0.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/svrx/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /packages/svrx/README.md: -------------------------------------------------------------------------------- 1 | # @svrx/svrx 2 | 3 | > svrx core. 4 | 5 | svrx(server-x) is a platform built for efficient front-end development. 6 | 7 | See our [website](https://svrx.io/) for more information. 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm install --save-dev @svrx/svrx 13 | ``` -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/historyApiFallback/index.html: -------------------------------------------------------------------------------- 1 | hi 2 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/serve/demo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/serve/demo.js: -------------------------------------------------------------------------------- 1 | parseInt('123', 10); 2 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/serve/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svrxjs/svrx/7abfdc3331fbbf5d06d8fd04eb4f82d00e3c683e/packages/svrx/__tests__/fixture/plugin/serve/demo.png -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/svrx-plugin-depend/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | name: 'test', 4 | priority: 100, 5 | configSchema: { 6 | limit: { 7 | type: 'number', 8 | default: 100, 9 | }, 10 | }, 11 | assets: { 12 | style: ['./assets/index.css'], 13 | script: ['./assets/index.js'], 14 | }, 15 | hooks: { 16 | async onRoute(ctx, next, { config }) { 17 | const limit = config.get('limit'); 18 | ctx.set('X-Svrx-Limit', limit); 19 | await next(); 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/svrx-plugin-depend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svrx-plugin-depend", 3 | "version": "0.0.1", 4 | "keywords": [ 5 | "svrx", 6 | "plugin" 7 | ], 8 | "license": "MIT", 9 | "main": "index.js", 10 | "dependencies": { 11 | "lodash.noop": "3.0.1" 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/svrx-plugin-no-package/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'no-package', 3 | hooks: { 4 | async onRoute(ctx, next, { config }) { 5 | const limit = config.get('limit'); 6 | ctx.set('X-Svrx-Limit', limit); 7 | await next(); 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/svrx-plugin-test/assets/index.css: -------------------------------------------------------------------------------- 1 | body{background: black} -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/svrx-plugin-test/assets/index.js: -------------------------------------------------------------------------------- 1 | console.log('svrx-plugin-test') -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/svrx-plugin-test/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'test', 3 | priority: 100, 4 | configSchema: { 5 | limit: { 6 | type: 'number', 7 | default: 100, 8 | }, 9 | }, 10 | assets: { 11 | style: ['./assets/index.css'], 12 | script: ['./assets/index.js'], 13 | }, 14 | hooks: { 15 | async onRoute(ctx, next, { config }) { 16 | const limit = config.get('limit'); 17 | ctx.set('X-Svrx-Limit', limit); 18 | await next(); 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/plugin/svrx-plugin-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svrx-plugin-test", 3 | "version": "0.0.1", 4 | "keywords": [ 5 | "svrx", 6 | "plugin" 7 | ], 8 | "license": "MIT", 9 | "main": "./index.js", 10 | "engines": { 11 | "svrx": "^0.0.1" 12 | }, 13 | "dependencies": {} 14 | } 15 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/router/rule.normal.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | get('/hello/:name').to.send('hello world'); 4 | 5 | get('/normal/:id').json({ code: 200 }); 6 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/router/rule.require.js: -------------------------------------------------------------------------------- 1 | const bodyParserRelative = require('./node_modules/koa-bodyparser'); //relative path 2 | const bodyParser = require('koa-bodyparser'); 3 | 4 | post('/test/post').to.handle(bodyParser()).handle((ctx) => { 5 | ctx.type = 'html'; 6 | ctx.body = ctx.request.body; 7 | }); 8 | post('/test/post/relative').to.handle(bodyParserRelative()).handle((ctx) => { 9 | ctx.type = 'html'; 10 | ctx.body = ctx.request.body; 11 | }); 12 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/fixture/router/static/index.html: -------------------------------------------------------------------------------- 1 | router -------------------------------------------------------------------------------- /packages/svrx/__tests__/spec/configure/builtinConfig.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const Option = require('../../../lib/configure/option'); 3 | const CONFIGS = require('../../../lib/config-list'); 4 | const { BUILTIN_PLUGIN } = require('../../../lib/constant'); 5 | const defaults = require('../../../lib/util/jsonSchemaDefaults'); 6 | const { createServer } = require('../../util'); 7 | 8 | const BUILTIN_DEFAULTS = defaults({ 9 | type: 'object', 10 | properties: CONFIGS, 11 | }); 12 | 13 | describe('Builtin Configs', () => { 14 | it('should fill all default values', () => { 15 | const server = createServer(); 16 | Object.keys(CONFIGS).forEach((key) => { 17 | const value = CONFIGS[key]; 18 | if (value.default !== undefined && key !== 'open' && key 19 | !== 'livereload') { 20 | expect(server.config.get(key)).to.eql(value.default); 21 | } 22 | }); 23 | }); 24 | 25 | it('should enable all builtin plugins by default', () => { 26 | const server = createServer(); 27 | const plugins = server.config.getPlugins(); 28 | expect(plugins.length).to.eql(BUILTIN_PLUGIN.length); 29 | expect(plugins.map((p) => p.getInfo('name'))).to.eql(BUILTIN_PLUGIN); 30 | }); 31 | 32 | it('should concat array values from CLI and RC', () => { 33 | const server = createServer({ 34 | proxy: [{ a: 'a' }], 35 | }, { 36 | proxy: [{ b: 'b' }], 37 | }); 38 | expect(server.config.get('proxy')).to.eql([{ a: 'a' }, { b: 'b' }]); 39 | }); 40 | }); 41 | 42 | describe('Config get', () => { 43 | const server = createServer({ 44 | port: 3000, 45 | plugins: [ 46 | { 47 | name: 'test', 48 | version: '0.0.1', 49 | inplace: true, 50 | configSchema: { 51 | foo: { 52 | type: 'string', 53 | default: 'bar', 54 | }, 55 | }, 56 | options: { 57 | op: 123, 58 | }, 59 | }, 60 | ], 61 | }); 62 | const { config } = server; 63 | const testPlugin = config.getPlugin('test'); 64 | 65 | it('should get builtin value corrently', () => { 66 | expect(config.get('port')).to.equal(3000); 67 | }); 68 | 69 | it('should get plugin info corrently', () => { 70 | expect(testPlugin.getInfo('version')).to.equal('0.0.1'); 71 | }); 72 | 73 | it('should get plugin option corrently', () => { 74 | expect(testPlugin.get('op')).to.equal(123); 75 | }); 76 | 77 | it('should get builtin options in plugin config with $', () => { 78 | expect(testPlugin.get('$.port')).to.equal(3000); 79 | }); 80 | 81 | it('should get all options (includes the defaults) when there\'s no path', () => { 82 | expect(config.get()).to.eql({ 83 | ...BUILTIN_DEFAULTS, port: 3000, open: false, livereload: false, 84 | }); 85 | }); 86 | 87 | it('should get all plugin options (includes the defaults) when there\'s no path', async () => { 88 | await server.setup(); 89 | expect(testPlugin.get()).to.eql({ 90 | op: 123, 91 | foo: 'bar', // defaults 92 | }); 93 | }); 94 | 95 | it('should return schema correctly', () => { 96 | expect(config.getSchema()).to.eql(CONFIGS); 97 | }); 98 | 99 | it('should return all external plugins when getExternalPlugins()', () => { 100 | expect(config.getExternalPlugins().map((p) => p.getInfo('name'))).to.eql(['test']); 101 | }); 102 | }); 103 | 104 | describe('Config set', () => { 105 | const server = createServer({ 106 | port: 3000, 107 | plugins: [ 108 | { 109 | name: 'test', 110 | version: '0.0.1', 111 | inplace: true, 112 | configSchema: { 113 | foo: { 114 | type: 'string', 115 | default: 'bar', 116 | }, 117 | }, 118 | options: { 119 | op: 123, 120 | }, 121 | }, 122 | ], 123 | }); 124 | const { config } = server; 125 | const testPlugin = config.getPlugin('test'); 126 | 127 | it('should set builtin value correctly', () => { 128 | config.set('port', 4000); 129 | expect(config.get('port')).to.equal(4000); 130 | config.set('port', 3000); 131 | }); 132 | 133 | it('should set plugin option correctly', () => { 134 | testPlugin.set('op', 321); 135 | testPlugin.set('other', 'other info'); 136 | expect(testPlugin.get('op')).to.equal(321); 137 | expect(testPlugin.get('other')).to.equal('other info'); 138 | }); 139 | 140 | it('should set builtin values in object correctly', () => { 141 | config.builtinsSet({ 142 | port: 4000, 143 | https: true, 144 | }); 145 | expect(config.get('port')).to.equal(4000); 146 | expect(config.get('https')).to.equal(true); 147 | config.builtinsSet({ 148 | port: 3000, 149 | https: false, 150 | }); 151 | }); 152 | }); 153 | 154 | describe('Functions', () => { 155 | let builtinConfig; 156 | before(async () => { 157 | const server = createServer({ 158 | port: 3000, 159 | plugins: [ 160 | { 161 | name: 'test', 162 | version: '0.0.1', 163 | inplace: true, 164 | configSchema: { 165 | foo: { 166 | type: 'string', 167 | default: 'bar', 168 | }, 169 | }, 170 | options: { 171 | op: 123, 172 | }, 173 | }, 174 | ], 175 | }); 176 | builtinConfig = server.config; 177 | }); 178 | 179 | it('should keep #watch() on configs', (done) => { 180 | const release = builtinConfig.watch((evt) => { 181 | expect(evt.affect()).to.equal(true); 182 | expect(evt.affect('watch.b.c')).to.equal(true); 183 | expect(evt.affect('watch')).to.equal(true); 184 | expect(evt.affect('watch.c')).to.equal(false); 185 | release(); 186 | done(); 187 | }); 188 | builtinConfig.set('watch.b.c', 'world'); 189 | }); 190 | 191 | it('should delete a config after #del()', () => { 192 | builtinConfig.set('test.del.item', 'hello'); 193 | expect(builtinConfig.get('test.del.item')).to.equal('hello'); 194 | builtinConfig.del('test.del.item'); 195 | expect(builtinConfig.get('test.del.item')).to.eql(undefined); 196 | 197 | builtinConfig.del('none.exists.config'); 198 | expect(builtinConfig.get('none.exists.config')).to.eql(undefined); 199 | }); 200 | 201 | it('should splice an array config after #splice()', () => { 202 | builtinConfig.set('test.splice.item', [1, 2, 3]); 203 | builtinConfig.splice('test.splice.item', 0, 1); 204 | expect(builtinConfig.get('test.splice.item')).to.eql([2, 3]); 205 | }); 206 | }); 207 | 208 | describe('Config Validate', () => { 209 | it('should validate single type configs', () => { 210 | const option = new Option({ 211 | https: 3000, // should be boolean 212 | port: 'port', // should be number 213 | svrx: 123, // should be string 214 | urls: 'should be object', // should be object 215 | }); 216 | const errors = option._validate(CONFIGS); 217 | expect(errors).not.to.equal(null); 218 | expect(errors.length).to.equal(4); 219 | expect(errors[0]).to.equal('Config Error: .https should be boolean'); 220 | expect(errors[1]).to.equal('Config Error: .port should be number'); 221 | expect(errors[2]).to.equal('Config Error: .svrx should be string'); 222 | expect(errors[3]).to.equal('Config Error: .urls should be object'); 223 | }); 224 | 225 | it('should validate multi type configs', () => { 226 | const option = new Option({ 227 | proxy: 123, // boolean,array,object 228 | open: ['a', 'b'], // boolean,string 229 | serve: 'string', 230 | }); 231 | const errors = option._validate(CONFIGS); 232 | expect(errors).not.to.equal(null); 233 | expect(errors.length).to.equal(3); 234 | expect(errors[0]) 235 | .to 236 | .equal('Config Error: .open should be boolean or string'); 237 | expect(errors[1]) 238 | .to 239 | .equal('Config Error: .proxy should be boolean or object or array'); 240 | expect(errors[2]) 241 | .to 242 | .equal('Config Error: .serve should be boolean or object'); 243 | }); 244 | 245 | it('should log the error path correctly', () => { 246 | const option = new Option({ 247 | serve: { 248 | base: 123, // string 249 | }, 250 | livereload: { 251 | exclude: true, 252 | }, 253 | }); 254 | const errors = option._validate(CONFIGS); 255 | expect(errors).not.to.equal(null); 256 | expect(errors.length).to.equal(2); 257 | expect(errors[0]) 258 | .to 259 | .equal('Config Error: .livereload.exclude should be string or array'); 260 | expect(errors[1]).to.equal('Config Error: .serve.base should be string'); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/spec/configure/inline.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const libPath = require('path'); 3 | const fs = require('fs'); 4 | const sinon = require('sinon'); 5 | const { createServer } = require('../../util'); 6 | require('../../../lib/configure/index'); // required for module get 7 | 8 | const TEST_PLUGIN_PATH = libPath.join(__dirname, '../../fixture/plugin/svrx-plugin-test'); 9 | const NO_PACKAGE_PLUGIN_PATH = libPath.join(__dirname, '../../fixture/plugin/svrx-plugin-no-package'); 10 | 11 | describe('Inline/RC File Config', () => { 12 | describe('plugins', () => { 13 | it('should pick string plugins correctly', () => { 14 | const server = createServer({ 15 | plugins: [ 16 | 'test', 'demo', 17 | ], 18 | }); 19 | const testPlugin = server.config.getPlugin('test'); 20 | const demoPlugin = server.config.getPlugin('demo'); 21 | expect(testPlugin).not.to.be(undefined); 22 | expect(demoPlugin).not.to.be(undefined); 23 | }); 24 | 25 | it('should pick obj plugins correctly', () => { 26 | const server = createServer({ 27 | plugins: [ 28 | { 29 | name: 'test', 30 | }, 31 | ], 32 | }); 33 | const testPlugin = server.config.getPlugin('test'); 34 | expect(testPlugin).not.to.be(undefined); 35 | }); 36 | 37 | // pass plugin by path 38 | describe('Local plugin name parse', () => { 39 | it('should parse normal plugin name correctly', () => { 40 | const server = createServer({ 41 | plugins: [ 42 | { 43 | path: TEST_PLUGIN_PATH, 44 | }, 45 | ], 46 | }); 47 | const testPlugin = server.config.getPlugin('test'); 48 | expect(testPlugin).not.to.be(undefined); 49 | }); 50 | it('should get plugin name without package.json correctly', () => { 51 | const server = createServer({ 52 | plugins: [ 53 | { 54 | path: NO_PACKAGE_PLUGIN_PATH, 55 | }, 56 | ], 57 | }); 58 | const testPlugin = server.config.getPlugin('no-package'); 59 | expect(testPlugin).not.to.be(undefined); 60 | }); 61 | it('should parse plugin name from path correctly', () => { 62 | const fakes = [ 63 | { path: '/fake/path/for/normal/plugin', name: 'svrx-plugin-foo', pluginName: 'foo' }, 64 | { path: '/fake/path/for/normal/dash/plugin', name: 'svrx-plugin-foo-bar', pluginName: 'foo-bar' }, 65 | { path: '/fake/path/for/scope/plugin', name: '@scope/svrx-plugin-foo', pluginName: '@scope/foo' }, 66 | { path: '/fake/path/for/scope/dash/plugin', name: '@scope/svrx-plugin-foo-bar', pluginName: '@scope/foo-bar' }, 67 | ]; 68 | const moduleId = '/svrx/packages/svrx/lib/configure/index.js'; 69 | 70 | fakes.forEach((fake) => { 71 | const fakePackagePath = libPath.join(fake.path, 'package.json'); 72 | const existsSyncStub = sinon.stub(fs, 'existsSync'); 73 | const configModule = module.children.find((mod) => mod.id.split(libPath.sep).join('/').endsWith(moduleId)); 74 | const requireStub = sinon.stub(configModule, 'require'); 75 | 76 | // fake functions 77 | requireStub.withArgs(fakePackagePath).callsFake(() => ({ name: fake.name })); 78 | existsSyncStub.withArgs(fakePackagePath).callsFake(() => true); 79 | 80 | const server = createServer({ 81 | plugins: [{ path: fake.path }], 82 | }); 83 | const testPlugin = server.config.getPlugin(fake.pluginName); 84 | expect(testPlugin).not.to.be(undefined); 85 | requireStub.restore(); 86 | existsSyncStub.restore(); 87 | }); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/spec/configure/pluginConfig.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const libPath = require('path'); 3 | const sinon = require('sinon'); 4 | const { createServer } = require('../../util'); 5 | 6 | const TEST_PLUGIN_PATH = libPath.join(__dirname, '../../fixture/plugin/svrx-plugin-test'); 7 | 8 | function requireEnsure(path) { 9 | delete require.cache[path]; 10 | /* eslint-disable global-require, import/no-dynamic-require */ 11 | return require(path); 12 | } 13 | 14 | describe('Plugin Config', () => { 15 | describe('functions', () => { 16 | let testPlugin; 17 | let builtinConfig; 18 | before(async () => { 19 | const server = createServer({ 20 | plugins: [{ 21 | path: TEST_PLUGIN_PATH, 22 | }], 23 | }); 24 | await server.setup(); 25 | testPlugin = server.config.getPlugin('test'); 26 | testPlugin = server.config.getPlugin('test'); 27 | builtinConfig = server.config; 28 | }); 29 | 30 | it('should return value correctly when #get()', () => { 31 | expect(testPlugin.get('none-exist')).to.equal(undefined); 32 | expect(testPlugin.get('limit')).to.equal(100); 33 | expect(testPlugin.get('$.port')).to.equal(8000); // builtin config 34 | }); 35 | 36 | it('should set value correctly when #set()', () => { 37 | testPlugin.set('foo', 'bar'); 38 | testPlugin.set('a.b', 'ab'); 39 | testPlugin.set('c.d', { obj: 'obj' }); 40 | expect(testPlugin.get('foo')).to.equal('bar'); 41 | expect(testPlugin.get('a.b')).to.equal('ab'); 42 | expect(testPlugin.get('c.d')).to.eql({ obj: 'obj' }); 43 | }); 44 | 45 | it('should not modify builtin configs by #set()', () => { 46 | testPlugin.set('$.port', 3000); 47 | expect(testPlugin.get('$.port')).to.not.equal(3000); 48 | }); 49 | 50 | it('should keep #watch() on configs', (done) => { 51 | const release = testPlugin.watch((evt) => { 52 | expect(evt.affect()).to.equal(true); 53 | expect(evt.affect('watch.b.c')).to.equal(true); 54 | expect(evt.affect('watch')).to.equal(true); 55 | expect(evt.affect('watch.c')).to.equal(false); 56 | release(); 57 | done(); 58 | }); 59 | testPlugin.set('watch.b.c', 'world'); 60 | }); 61 | 62 | it('should keep #watch($.port) on builtin configs', (done) => { 63 | const release = testPlugin.watch('$.port', (evt) => { 64 | expect(evt.affect()).to.equal(true); 65 | release(); 66 | done(); 67 | }); 68 | builtinConfig.set('port', 3000); 69 | }); 70 | 71 | it('should keep #watch($) on builtin configs', (done) => { 72 | const release = testPlugin.watch('$', (evt) => { 73 | expect(evt.affect('port')).to.equal(true); 74 | release(); 75 | done(); 76 | }); 77 | builtinConfig.set('port', 4000); 78 | }); 79 | 80 | it('should keep #watch($.a) multi-level builtin configs', (done) => { 81 | const release = testPlugin.watch('$.a', (evt) => { 82 | expect(evt.affect('b.c')).to.equal(true); 83 | expect(evt.affect('b.d')).to.equal(false); 84 | release(); 85 | done(); 86 | }); 87 | builtinConfig.set('a.b.c', 3000); 88 | }); 89 | 90 | it('should delete a config after #del()', () => { 91 | testPlugin.set('test.del.item', 'hello'); 92 | expect(testPlugin.get('test.del.item')).to.equal('hello'); 93 | testPlugin.del('test.del.item'); 94 | expect(testPlugin.get('test.del.item')).to.eql(undefined); 95 | 96 | testPlugin.del('none.exists.config'); 97 | expect(testPlugin.get('none.exists.config')).to.eql(undefined); 98 | }); 99 | 100 | it('should splice an array config after #splice()', () => { 101 | testPlugin.set('test.splice.item', [1, 2, 3]); 102 | testPlugin.splice('test.splice.item', 0, 1); 103 | expect(testPlugin.get('test.splice.item')).to.eql([2, 3]); 104 | }); 105 | }); 106 | 107 | it('should return default values when get plugin(load from path) option', async () => { 108 | const server = createServer({ 109 | plugins: [{ 110 | path: TEST_PLUGIN_PATH, 111 | }], 112 | }); 113 | await server.setup(); 114 | const testPlugin = server.config.getPlugin('test'); 115 | expect(testPlugin.get('limit')).to.equal(100); 116 | }); 117 | 118 | it('should return default values when get plugin(load from remote) option', async () => { 119 | const server = createServer({ 120 | plugins: ['remote'], 121 | }); 122 | const fakeLoadOne = sinon.fake.resolves({ 123 | name: 'remote', 124 | path: TEST_PLUGIN_PATH, 125 | module: requireEnsure(TEST_PLUGIN_PATH), 126 | version: requireEnsure(TEST_PLUGIN_PATH).version, 127 | pluginConfig: server.config.getPlugin('remote'), 128 | }); 129 | 130 | const pluginDetail = await fakeLoadOne(); 131 | await server.system.buildOne(pluginDetail); 132 | 133 | const testPlugin = server.config.getPlugin('remote'); 134 | expect(testPlugin.get('limit')).to.equal(100); 135 | expect(testPlugin.get('$.port')).to.equal(8000); 136 | sinon.restore(); 137 | }); 138 | 139 | it('should return all builtin options when get(\'$\')', () => { 140 | const server = createServer({ 141 | plugins: ['test'], 142 | }); 143 | const { config } = server; 144 | expect(config.getPlugin('test').get('$')).to.eql( 145 | config.get(), 146 | ); 147 | }); 148 | 149 | it('should return plugin schema using getSchema()', async () => { 150 | const server = createServer({ 151 | plugins: [{ 152 | path: TEST_PLUGIN_PATH, 153 | }], 154 | }); 155 | await server.setup(); 156 | const { config } = server; 157 | expect(config.getPlugin('test').getSchema()).to.eql({ limit: { type: 'number', default: 100 } }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/spec/configure/ui.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect.js'); 2 | const { createServer } = require('../../util'); 3 | 4 | describe('UI Configs', () => { 5 | let uiClientConfig = null; 6 | const server = createServer({ 7 | plugins: [ 8 | { 9 | name: 'ui', 10 | inplace: true, 11 | hooks: { 12 | onCreate({ config }) { 13 | uiClientConfig = config; 14 | }, 15 | }, 16 | }, 17 | ], 18 | }); 19 | const { config } = server; 20 | 21 | it('should consider ui as external plugin', () => { 22 | expect(config.getExternalPlugins().map((p) => p.getInfo('name'))).to.eql(['ui']); 23 | }); 24 | it('should return ui config as builtin config in client side', async () => { 25 | await server.setup(); 26 | // uiClientConfig is actually builtin config here 27 | expect(uiClientConfig.get()).to.eql(config.get()); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/spec/svrx.injector.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { Duplex } = require('stream'); 3 | const expect = require('expect.js'); 4 | const Svrx = require('../../lib/svrx'); 5 | 6 | function bufferToStream(buffer) { 7 | const stream = new Duplex(); 8 | stream.push(buffer); 9 | stream.push(null); 10 | return stream; 11 | } 12 | 13 | describe('Injector', () => { 14 | describe('Basic', () => { 15 | const svrx = new Svrx({}); 16 | const { injector } = svrx; 17 | const MARK_TESTING = '__svrx_testing__'; 18 | 19 | injector.add('style', { content: 'body{padding:10px}' }); 20 | injector.add('style', { content: 'body{color:black}' }); 21 | injector.add('script', { content: 'window.test=true;' }); 22 | injector.add('style', { 23 | content: MARK_TESTING, 24 | test(referer) { 25 | return /\.md$/.test(referer); 26 | }, 27 | }); 28 | 29 | it('Integration: Basic', (done) => { 30 | request(svrx.callback()) 31 | .get(svrx.config.get('urls.script')) 32 | .set('accept-encoding', 'identity') 33 | .expect(/window\.test=true/, (err) => { 34 | if (err) return done(err); 35 | return request(svrx.callback()) 36 | .get(svrx.config.get('urls.style')) 37 | .set('accept-encoding', 'identity') 38 | .expect(/body\{padding:10px\}/) 39 | .end(done); 40 | }); 41 | }); 42 | 43 | it('valid file will return no error', () => { 44 | expect(() => { 45 | injector.add('style', { filename: 'content_not_exsits.js' }); 46 | }).to.not.throwError(); 47 | }); 48 | 49 | it('Integration: style join', () => request(svrx.callback()) 50 | .get(svrx.config.get('urls.style')) 51 | .set('accept-encoding', 'identity') 52 | .expect(/body\{padding:10px\}\nbody\{color:black\}/)); 53 | 54 | it('Integration: Gzip content-encoding', () => request(svrx.callback()) 55 | .get(svrx.config.get('urls.script')) 56 | .set('accept-encoding', 'gzip') 57 | .expect('content-encoding', 'gzip') 58 | .expect(/window\.test=true/)); 59 | 60 | it('Integration: Injection Testing', (done) => { 61 | request(svrx.callback()) 62 | .get(svrx.config.get('urls.style')) 63 | .set('accept-encoding', 'identity') 64 | .expect(/body\{padding:10px\}/) 65 | .expect((res) => { 66 | expect(res.text).to.not.match(new RegExp(MARK_TESTING)); 67 | }) 68 | .end((err) => { 69 | if (err) return done(err); 70 | return request(svrx.callback()) 71 | .get(svrx.config.get('urls.style')) 72 | .set('accept-encoding', 'identity') 73 | .set('Referer', 'test.md') 74 | .expect(/body\{padding:10px\}/) 75 | .expect(new RegExp(MARK_TESTING), done); 76 | }); 77 | }); 78 | }); 79 | 80 | describe('Transform content', () => { 81 | const svrx = new Svrx({ 82 | port: 8001, 83 | middlewares: [ 84 | { 85 | onCreate: () => async (ctx, next) => { 86 | switch (ctx.url) { 87 | case '/content': 88 | ctx.set('Content-Type', 'text/html'); 89 | ctx.body = ''; 90 | break; 91 | case '/buffer': 92 | ctx.set('Content-Type', 'text/html'); 93 | ctx.body = Buffer.from(''); 94 | break; 95 | case '/stream': 96 | ctx.set('Content-Type', 'text/html'); 97 | ctx.body = bufferToStream(Buffer.from('')); 98 | break; 99 | default: 100 | next(); 101 | } 102 | }, 103 | }, 104 | ], 105 | }); 106 | const { injector } = svrx; 107 | 108 | it('replace should work', () => { 109 | injector.replace('', (cap) => `${cap}`); 110 | 111 | return request(svrx.callback()) 112 | .get('/content') 113 | .set('accept-encoding', 'identity') 114 | .expect(/ 182 | 183 | mark`; 184 | break; 185 | case '/only-body': 186 | ctx.set('Content-Type', 'text/html'); 187 | ctx.body = ''; 188 | break; 189 | case '/only-body-stream': 190 | ctx.set('Content-Type', 'text/html'); 191 | ctx.body = bufferToStream(Buffer.from('')); 192 | break; 193 | case '/none-of-both': 194 | ctx.set('Content-Type', 'text/html'); 195 | ctx.body = bufferToStream(Buffer.from('Hello,World')); 196 | break; 197 | default: 198 | next(); 199 | } 200 | }, 201 | }, 202 | ], 203 | }); 204 | 205 | 206 | it('only last body will be inject', (done) => { 207 | request(svrx.callback()) 208 | .get('/doc-write-body') 209 | .expect(new RegExp('mark { 215 | request(svrx.callback()) 216 | .get('/only-body') 217 | .expect(new RegExp(`src="${svrx.config.get('urls.script')}"`)) 218 | .expect(new RegExp(`href="${svrx.config.get('urls.style')}"`)) 219 | .end(done); 220 | }); 221 | it('stream:missing head should inject style at body', (done) => { 222 | request(svrx.callback()) 223 | .get('/only-body-stream') 224 | .expect(new RegExp(`src="${svrx.config.get('urls.script')}"`)) 225 | .expect(new RegExp(`href="${svrx.config.get('urls.style')}"`)) 226 | .end(done); 227 | }); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/spec/svrx.io.js: -------------------------------------------------------------------------------- 1 | const ioClient = require('socket.io-client'); 2 | const supertest = require('supertest'); 3 | const expect = require('expect.js'); 4 | const http = require('http'); 5 | 6 | const { IO_PATH } = require('../../lib/shared/consts'); 7 | const Middleware = require('../../lib/middleware'); 8 | const { createServer } = require('../util'); 9 | const IO = require('../../lib/io'); 10 | 11 | describe('IO', () => { 12 | const server = http.createServer(); 13 | const middleware = new Middleware(); 14 | const io = new IO({ server, middleware }); 15 | after((done) => { 16 | server.close(done); 17 | }); 18 | 19 | it('io.events', (done) => { 20 | server.listen(8008, () => { 21 | const socket = ioClient.connect('http://localhost:8008', { 22 | path: IO_PATH, 23 | }); 24 | const handler = () => { 25 | io.emit('hello', 'world'); 26 | }; 27 | 28 | io.on('svrx', (payload) => { 29 | expect(payload).to.equal('hello'); 30 | socket.close(); 31 | io.off('svrx'); 32 | done(); 33 | }); 34 | io._io.on('connection', handler); 35 | socket.on('$message', (evt) => { 36 | expect(evt.type).to.equal('hello'); 37 | expect(evt.payload).to.equal('world'); 38 | socket.emit('$message', { type: 'svrx', payload: 'hello' }); 39 | }); 40 | }); 41 | }); 42 | 43 | it('io.call', async () => { 44 | io.register('hello', async (payload) => `hi ${payload}`); 45 | expect(await io.call('hello', 'leeluolee')).to.equal('hi leeluolee'); 46 | }); 47 | 48 | it('io.call limit error', async () => { 49 | expect(() => { 50 | for (let i = 0; i < 1000; i += 1) { 51 | io.register(`hello${i}`, async (payload) => `hi ${i} ${payload}`); 52 | } 53 | }).to.throwError(/max service size limit exceeded/); 54 | }); 55 | 56 | it('io.call:xhr', async () => { 57 | const svrx = createServer({ 58 | root: __dirname, 59 | }); 60 | svrx.io.register('hello', async (payload) => `hi ${payload}`); 61 | await svrx.setup(); 62 | return supertest(svrx.callback()) 63 | .post(IO_PATH) 64 | .send({ serviceName: 'hello', payload: 'svrx' }) 65 | .set('Accept', 'application/json') 66 | .expect('hi svrx'); 67 | }); 68 | 69 | it('io.call:xhr error', async () => { 70 | const svrx = createServer({ 71 | root: __dirname, 72 | }); 73 | svrx.io.register('error', async (payload) => { 74 | throw Error(`hi ${payload}`); 75 | }); 76 | await svrx.setup(); 77 | return supertest(svrx.callback()) 78 | .post(IO_PATH) 79 | .send({ serviceName: 'error', payload: 'svrx' }) 80 | .set('Accept', 'application/json') 81 | .expect(500) 82 | .expect('hi svrx'); 83 | }); 84 | 85 | it('io.call unregist', async () => { 86 | try { 87 | await io.call('something-unregist'); 88 | } catch (e) { 89 | expect(e.message).to.match(/unregisted service/); 90 | } 91 | }); 92 | 93 | it('io.config', async () => { 94 | const svrx = createServer({ 95 | root: __dirname, 96 | plugins: [ 97 | { 98 | name: 'hello-world', 99 | inplace: true, 100 | options: { 101 | name: 'orpheus', 102 | }, 103 | }, 104 | ], 105 | }); 106 | await svrx.setup(); 107 | 108 | expect( 109 | await svrx.io.call('$.config', { 110 | command: 'get', 111 | scope: 'hello-world', 112 | params: ['name'], 113 | }), 114 | ).to.equal('orpheus'); 115 | 116 | await svrx.io.call('$.config', { 117 | command: 'set', 118 | scope: 'hello-world', 119 | params: ['name', 'leeluolee'], 120 | }); 121 | 122 | expect( 123 | await svrx.io.call('$.config', { 124 | command: 'get', 125 | scope: 'hello-world', 126 | params: ['name'], 127 | }), 128 | ).to.equal('leeluolee'); 129 | 130 | expect( 131 | await svrx.io.call('$.config', { 132 | command: 'get', 133 | params: ['root'], 134 | }), 135 | ).to.equal(__dirname); 136 | 137 | let err; 138 | try { 139 | await svrx.io.call('$.config', { 140 | scope: 'not-exsits', 141 | command: 'get', 142 | params: ['hello'], 143 | }); 144 | } catch (e) { 145 | err = e; 146 | } 147 | expect(err).to.match(/plugin not-exsits/); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/spec/svrx.plugin.proxy.js: -------------------------------------------------------------------------------- 1 | const supertest = require('supertest'); 2 | const { Buffer } = require('buffer'); 3 | const { createServer } = require('../util'); 4 | const { gzip } = require('../../lib/util/gzip'); 5 | 6 | const PROXY_SERVER = 'http://localhost:9003'; 7 | const PROXY_SERVER_HTTPS = 'https://localhost:9004'; 8 | const proxyServer = createServer({ 9 | port: 9003, 10 | plugins: [ 11 | { 12 | name: 'proxy-action-test', 13 | inplace: true, 14 | hooks: { 15 | async onCreate({ router }) { 16 | const { route } = router; 17 | 18 | const htmlContent = await gzip(Buffer.from('hello')); 19 | route(({ get }) => { 20 | get('/api(.*)').to.send('proxied'); 21 | get('/wapi(.*)').to.send('proxied wapi'); 22 | get('/origin/api(.*)').to.handle((ctx) => { 23 | if (ctx.request.headers.host === 'localhost:9003') { 24 | ctx.body = 'changeOrigin proxied'; 25 | } 26 | }); 27 | get('/dynamic/host/:port').to.send('dynamic proxied'); 28 | get('/foo/abc').to.send('abc'); 29 | get('/foo/xyz').to.send('xyz'); 30 | get('/bar/page-1').to.send('page-1'); 31 | get('/other/path').to.send('other'); 32 | get('/gzip').to 33 | .header({ 34 | 'Content-Encoding': 'gzip', 35 | 'Content-Type': 'html', 36 | }) 37 | .send(htmlContent, { gzip: true }); 38 | }); 39 | }, 40 | }, 41 | }, 42 | ], 43 | }); 44 | const proxyServerHttps = createServer({ 45 | port: 9004, 46 | https: true, 47 | plugins: [ 48 | { 49 | name: 'proxy-action-test', 50 | inplace: true, 51 | hooks: { 52 | async onCreate({ router }) { 53 | const { route } = router; 54 | route(({ get }) => { 55 | get('/secure/api(.*)').to.send('secure proxied'); 56 | }); 57 | }, 58 | }, 59 | }, 60 | ], 61 | }); 62 | 63 | describe('Proxy', () => { 64 | const noProxyServer = createServer({ 65 | proxy: false, 66 | }); 67 | 68 | let agt; 69 | before((done) => { 70 | noProxyServer.setup().then(() => { 71 | agt = supertest(noProxyServer.callback()); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should proxy nothing when proxy is set to false', () => agt 77 | .get('/api/test') 78 | .expect(404)); 79 | 80 | describe('Context is object', () => { 81 | const svrx = createServer({ 82 | proxy: { 83 | '/api/test': { 84 | target: PROXY_SERVER, 85 | }, 86 | '/foo/(abc|xyz)': { 87 | target: PROXY_SERVER, 88 | }, 89 | '/bar/page-[1-5]': { 90 | target: PROXY_SERVER, 91 | }, 92 | '/gzip': { 93 | target: PROXY_SERVER, 94 | }, 95 | }, 96 | }); 97 | let agent; 98 | before((done) => { 99 | Promise.all([ 100 | svrx.setup(), 101 | new Promise((resolve) => proxyServer.start(resolve)), 102 | ]).then(() => { 103 | agent = supertest(svrx.callback()); 104 | done(); 105 | }); 106 | }); 107 | after((done) => { 108 | proxyServer.close(done); 109 | }); 110 | 111 | it('should proxy path to a target server', () => agent 112 | .get('/api/test') 113 | .expect('proxied')); 114 | 115 | it('should support micromatch pattern \'logical or\'', () => agent 116 | .get('/foo/abc') 117 | .expect('abc')); 118 | 119 | it('should support micromatch pattern \'logical or\'', () => agent 120 | .get('/foo/xyz') 121 | .expect('xyz')); 122 | 123 | it('should support micromatch pattern \'regex character classes\'', () => agent 124 | .get('/bar/page-1') 125 | .expect('page-1')); 126 | 127 | it('should return 404 when not match pattern', () => agent 128 | .get('/no/match') 129 | .expect(404)); 130 | 131 | // fixme 132 | // it('should set content-encoding to identity when resp is gzip', () => agent 133 | // .get('/gzip') 134 | // .set('accept-encoding', 'identity') 135 | // .expect('Content-Encoding', 'identity') 136 | // .expect(/hello/)); 137 | }); 138 | 139 | describe('Context is array', () => { 140 | const svrx = createServer({ 141 | proxy: [ 142 | { 143 | context: ['/api', '/wapi'], 144 | target: PROXY_SERVER, 145 | }, 146 | { 147 | // context: [], no context supplied will match any path 148 | target: PROXY_SERVER, 149 | }, 150 | ], 151 | }); 152 | let agent; 153 | before((done) => { 154 | Promise.all([ 155 | svrx.setup(), 156 | new Promise((resolve) => proxyServer.start(resolve)), 157 | ]).then(() => { 158 | agent = supertest(svrx.callback()); 159 | done(); 160 | }); 161 | }); 162 | after((done) => { 163 | proxyServer.close(done); 164 | }); 165 | 166 | it('should proxy path to a target server', () => agent 167 | .get('/api/test') 168 | .expect('proxied')); 169 | 170 | it('should proxy path to a target server', () => agent 171 | .get('/wapi/test') 172 | .expect('proxied wapi')); 173 | 174 | it('should match any path if context is undefined ', () => agent 175 | .get('/other/path') 176 | .expect('other')); 177 | }); 178 | }); 179 | 180 | describe('Proxy Api', () => { 181 | const svrx = createServer({ 182 | plugins: [ 183 | { 184 | name: 'proxy-api-test', 185 | inplace: true, 186 | hooks: { 187 | async onRoute(ctx, next) { 188 | if (ctx.path !== '/api/test') { 189 | await next(); 190 | return; 191 | } 192 | await ctx.proxy(ctx, { 193 | target: PROXY_SERVER, 194 | }); 195 | }, 196 | }, 197 | }, 198 | ], 199 | }); 200 | let agent; 201 | before((done) => { 202 | Promise.all([ 203 | svrx.setup(), 204 | new Promise((resolve) => proxyServer.start(resolve)), 205 | ]).then(() => { 206 | agent = supertest(svrx.callback()); 207 | done(); 208 | }); 209 | }); 210 | after((done) => { 211 | proxyServer.close(done); 212 | }); 213 | 214 | it('should apend a proxy api to ctx', () => agent 215 | .get('/api/test') 216 | .expect('proxied')); 217 | }); 218 | 219 | describe('Proxy Action', async () => { 220 | const svrx = createServer({ 221 | plugins: [ 222 | { 223 | name: 'action-test', 224 | inplace: true, 225 | hooks: { 226 | async onCreate({ router }) { 227 | const { route } = router; 228 | route(({ get }) => { 229 | get('/api(.*)').to.proxy(PROXY_SERVER); 230 | get('/origin/api/test').to.proxy(PROXY_SERVER); 231 | get('/origin/api/noset').to.proxy(PROXY_SERVER, { 232 | changeOrigin: false, 233 | }); 234 | get('/rewrite/api(.*)').to.proxy(PROXY_SERVER, { 235 | pathRewrite: { 236 | '^/rewrite/api': '/api', 237 | }, 238 | }); 239 | get('/secure/api/test').to.proxy(PROXY_SERVER_HTTPS, { 240 | secure: false, 241 | }); 242 | get('/secure/api/noset').to.proxy(PROXY_SERVER_HTTPS); 243 | get('/dynamic/host/:port').to.proxy('http://localhost:{port}'); 244 | }); 245 | }, 246 | }, 247 | }, 248 | ], 249 | }); 250 | let agent; 251 | before((done) => { 252 | Promise.all([ 253 | svrx.setup(), 254 | new Promise((resolve) => proxyServer.start(resolve)), 255 | new Promise((resolve) => proxyServerHttps.start(resolve)), 256 | ]).then(() => { 257 | agent = supertest(svrx.callback()); 258 | done(); 259 | }); 260 | }); 261 | after((done) => { 262 | proxyServer.close(); 263 | proxyServerHttps.close(); 264 | done(); 265 | }); 266 | 267 | it('should proxy path to a target server', () => agent 268 | .get('/api/test') 269 | .expect('proxied')); 270 | 271 | it('should change the host header when set changeOrigin to true', () => agent 272 | .get('/origin/api/test') 273 | .expect('changeOrigin proxied')); 274 | 275 | it('should not change the host header when not set changeOrigin', () => agent 276 | .get('/origin/api/noset') 277 | .expect(404)); 278 | 279 | it('should rewrite the path after proxy', () => agent 280 | .get('/rewrite/api/test') 281 | .expect('proxied')); 282 | 283 | 284 | it('should receive ERROR from a https server without a valid SSL certificate', () => agent 285 | .get('/secure/api/noset') 286 | .expect(500)); // Internal Server Error: self signed certificate 287 | 288 | it('should work after set secure to false with server that has no valid SSL certificate', () => agent 289 | .get('/secure/api/test') 290 | .expect('secure proxied')); 291 | 292 | it('should work when set a dynamic target hostname', () => agent 293 | .get('/dynamic/host/9003') 294 | .expect('dynamic proxied')); 295 | }); 296 | -------------------------------------------------------------------------------- /packages/svrx/__tests__/util.js: -------------------------------------------------------------------------------- 1 | 2 | const Svrx = require('../lib/svrx'); 3 | 4 | async function getProxyServer() { 5 | return this; 6 | } 7 | 8 | 9 | function createServer(inlineOptions = {}, cliOptions = {}) { 10 | inlineOptions.livereload = false; 11 | inlineOptions.open = inlineOptions.open || false; 12 | return new Svrx(inlineOptions, cliOptions); 13 | } 14 | 15 | exports.getProxyServer = getProxyServer; 16 | exports.createServer = createServer; 17 | -------------------------------------------------------------------------------- /packages/svrx/index.js: -------------------------------------------------------------------------------- 1 | const Svrx = require('./lib/svrx'); 2 | 3 | function mapSvrxToExports(ctx) { 4 | function reload() { 5 | return ctx.io.emit('file:change', {}, true); 6 | } 7 | 8 | function start() { 9 | return new Promise((resolve) => ctx.start(resolve)); 10 | } 11 | 12 | function close() { 13 | return new Promise((resolve) => ctx.close(resolve)); 14 | } 15 | 16 | const forExports = {}; 17 | 18 | forExports.__svrx = ctx; 19 | 20 | forExports.start = start; 21 | forExports.close = close; 22 | forExports.reload = reload; 23 | 24 | forExports.on = ctx.events.on.bind(ctx.events); 25 | forExports.off = ctx.events.off.bind(ctx.events); 26 | forExports.emit = ctx.events.emit.bind(ctx.events); 27 | 28 | return forExports; 29 | } 30 | 31 | function create(options) { 32 | return mapSvrxToExports(new Svrx(options)); 33 | } 34 | 35 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 36 | 37 | /* istanbul ignore if */ 38 | if (process.platform === 'win32') { 39 | const rl = require('readline').createInterface({ // eslint-disable-line 40 | input: process.stdin, 41 | output: process.stdout, 42 | }); 43 | rl.on('SIGINT', () => { 44 | process.emit('SIGINT'); 45 | }); 46 | rl.on('SIGTERM', () => { 47 | process.emit('SIGTERM'); 48 | }); 49 | } 50 | 51 | process.on('SIGINT', () => { 52 | process.exit(); 53 | }); 54 | process.on('SIGTERM', () => { 55 | process.exit(); 56 | }); 57 | 58 | module.exports = create; 59 | 60 | create.create = create; 61 | -------------------------------------------------------------------------------- /packages/svrx/lib/config-list.js: -------------------------------------------------------------------------------- 1 | const { GROUPS } = require('./constant'); 2 | 3 | module.exports = { 4 | root: { 5 | type: 'string', 6 | default: process.cwd(), 7 | description: 'where to start svrx', 8 | defaultHint: 'default to the current working directory', 9 | group: GROUPS.CORE, 10 | cli: false, 11 | ui: false, 12 | }, 13 | svrx: { 14 | type: 'string', 15 | description: 'the version of svrx you want to use', 16 | defaultHint: 'default to the latest version installed locally', 17 | group: GROUPS.CORE, 18 | ui: false, 19 | }, 20 | path: { 21 | type: 'string', 22 | description: 'the path of local svrx core package(for development)', 23 | group: GROUPS.CORE, 24 | ui: false, 25 | }, 26 | registry: { 27 | type: 'string', 28 | description: 'the registry of npm', 29 | group: GROUPS.CORE, 30 | pattern: '^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)', 31 | }, 32 | port: { 33 | type: 'number', 34 | default: 8000, 35 | description: 'Specify a port number to listen for requests on', 36 | group: GROUPS.CORE, 37 | ui: false, 38 | }, 39 | https: { 40 | description: 'enable https', 41 | type: 'boolean', 42 | default: false, 43 | group: GROUPS.CORE, 44 | ui: false, 45 | }, 46 | route: { 47 | description: 'the path of routing config file', 48 | type: 'string', 49 | group: GROUPS.CORE, 50 | ui: false, 51 | }, 52 | plugin: { 53 | group: GROUPS.CORE, 54 | alias: 'p', 55 | description: 'Add a plugin by "[@{scope}/]{name}[@{version}][?{optionsQueryString}]"', 56 | anyOf: [ 57 | { type: 'string' }, 58 | { type: 'array', items: { type: 'string' } }, 59 | ], 60 | ui: false, 61 | }, 62 | urls: { 63 | type: 'object', 64 | cli: false, 65 | ui: false, 66 | properties: { 67 | style: { 68 | type: 'string', 69 | default: '/svrx/svrx-client.css', 70 | group: GROUPS.CORE, 71 | }, 72 | script: { 73 | type: 'string', 74 | default: '/svrx/svrx-client.js', 75 | group: GROUPS.CORE, 76 | }, 77 | external: { 78 | type: 'string', 79 | group: GROUPS.CORE, 80 | }, 81 | local: { 82 | type: 'string', 83 | group: GROUPS.CORE, 84 | }, 85 | ui: { 86 | type: 'string', 87 | group: GROUPS.CORE, 88 | }, 89 | }, 90 | }, 91 | plugins: { 92 | type: 'array', 93 | group: GROUPS.CORE, 94 | cli: false, 95 | ui: false, 96 | }, 97 | middlewares: { 98 | type: 'array', 99 | group: GROUPS.CORE, 100 | cli: false, 101 | ui: false, 102 | }, 103 | 104 | // built plugin configs 105 | historyApiFallback: { 106 | group: GROUPS.COMMON, 107 | description: 'Enable historyApiFallback middleware', 108 | anyOf: [{ 109 | title: 'enable historyApiFallback', 110 | type: 'boolean', 111 | }, { 112 | title: 'more configs of historyApiFallback(in object)', 113 | type: 'object', 114 | }], 115 | default: false, 116 | }, 117 | serve: { 118 | description: 'dev server configs', 119 | group: GROUPS.COMMON, 120 | default: true, 121 | anyOf: [ 122 | { 123 | title: 'enable dev server', 124 | type: 'boolean', 125 | }, 126 | { 127 | title: 'more configs of dev server', 128 | type: 'object', 129 | properties: { 130 | base: { 131 | type: 'string', 132 | description: 'where to serve static content from', 133 | }, 134 | index: { 135 | type: 'string', 136 | description: 'Name of the index file to serve automatically when visiting the root location', 137 | defaultHint: 'default to "index.html"', 138 | }, 139 | directory: { 140 | type: 'boolean', 141 | description: 'Enable serveIndex middleware', 142 | }, 143 | }, 144 | }, 145 | ], 146 | }, 147 | 148 | proxy: { 149 | description: 'proxy requests configs', 150 | group: GROUPS.COMMON, 151 | anyOf: [ 152 | { 153 | title: 'enable proxy', 154 | type: 'boolean', 155 | }, 156 | { 157 | title: 'more configs of proxy(in object)', 158 | type: 'object', 159 | }, 160 | { 161 | title: 'more configs of proxy(in array of object)', 162 | type: 'array', 163 | items: { 164 | type: 'object', 165 | }, 166 | }, 167 | ], 168 | }, 169 | 170 | livereload: { 171 | description: 'enable auto live reload', 172 | group: GROUPS.COMMON, 173 | default: true, 174 | ui: false, 175 | anyOf: [ 176 | { 177 | title: 'enable livereload', 178 | type: 'boolean', 179 | }, 180 | { 181 | title: 'more configs of livereload', 182 | type: 'object', 183 | properties: { 184 | exclude: { 185 | description: 'specify patterns to exclude from file watchlist', 186 | anyOf: [ 187 | { title: 'one pattern', type: 'string' }, 188 | { 189 | title: 'several pattern', 190 | type: 'array', 191 | items: { type: 'string' }, 192 | }, 193 | ], 194 | }, 195 | }, 196 | }, 197 | ], 198 | }, 199 | 200 | cors: { 201 | description: 'Cross-Origin Resource Sharing(CORS)', 202 | group: GROUPS.COMMON, 203 | default: true, 204 | anyOf: [ 205 | { 206 | title: 'enable cors', 207 | type: 'boolean', 208 | }, 209 | { 210 | title: 'more configs of cors(in object)', 211 | type: 'object', 212 | }, 213 | ], 214 | }, 215 | open: { 216 | description: 'open target page after server start', 217 | group: GROUPS.COMMON, 218 | default: 'local', 219 | anyOf: [ 220 | { 221 | title: 'enable auto browser opening', 222 | type: 'boolean', 223 | }, 224 | { 225 | title: 'open \'local\', \'external\' or other file name', 226 | type: 'string', 227 | }, 228 | ], 229 | ui: false, 230 | }, 231 | logger: { 232 | description: 'global logger setting', 233 | group: GROUPS.CORE, 234 | type: 'object', 235 | properties: { 236 | level: { 237 | type: 'string', 238 | enum: [ 239 | 'silent', 240 | 'notify', 241 | 'error', 242 | 'warn', 243 | 'debug', 244 | ], 245 | default: 'warn', 246 | description: 'set log level, predefined values: \'silent\',\'notify\',\'error\',\'warn\', \'debug\'', 247 | }, 248 | }, 249 | }, 250 | }; 251 | -------------------------------------------------------------------------------- /packages/svrx/lib/configure/builtinOption.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Option = require('./option'); 3 | 4 | class BuiltinOption extends Option { 5 | constructor(data) { 6 | const { cli = {}, rc = {} } = data; 7 | const options = BuiltinOption._merge(cli, rc); 8 | super(options); 9 | } 10 | 11 | /** 12 | * merged inline and rcfile options 13 | * addons has a lower priority 14 | * if the value type is array, the values will concat 15 | * if the value type is object, the values will be merged 16 | * merged 17 | * @param options 18 | * @param addons 19 | * @private 20 | */ 21 | static _merge(options = {}, addons = {}) { 22 | /* eslint-disable consistent-return */ 23 | const customizer = (objValue, srcValue) => { 24 | if (_.isArray(objValue)) { 25 | return objValue.concat(srcValue); 26 | } 27 | }; 28 | 29 | return _.mergeWith(_.cloneDeep(addons), options, customizer); 30 | } 31 | } 32 | 33 | module.exports = BuiltinOption; 34 | -------------------------------------------------------------------------------- /packages/svrx/lib/configure/option.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const Ajv = require('super-ajv'); 3 | const ajvErrorParse = require('ajv-errors'); 4 | const IModel = require('../model'); 5 | const logger = require('../util/logger'); 6 | 7 | class Option extends IModel { 8 | validate(configs = {}) { 9 | try { 10 | const errors = this._validate(configs); 11 | if (errors !== null) { 12 | errors.forEach((err) => { 13 | logger.error(err); 14 | }); 15 | process.exit(1); 16 | } 17 | } catch (e) { 18 | logger.warn(e); 19 | } 20 | } 21 | 22 | _validate(configs = {}) { 23 | const options = this.get(); 24 | const ajv = new Ajv({ 25 | allErrors: true, 26 | jsonPointers: true, 27 | }); 28 | 29 | // define custom types 30 | // fixme validate is not work for function values 31 | ajv.addType('function', { 32 | compile: () => (data) => _.isFunction(data), 33 | }); 34 | ajv.addType('compute', { 35 | compile: () => (data) => _.isFunction(data), 36 | }); 37 | 38 | // errors formatting 39 | ajvErrorParse(ajv); 40 | 41 | const valid = ajv.validate( 42 | { 43 | type: 'object', 44 | properties: configs, 45 | }, 46 | options, 47 | ); 48 | 49 | if (!valid) { 50 | const ajvErrors = ajv.errors; 51 | const formattedErrors = Option._format(ajvErrors); 52 | return formattedErrors.map((err) => `Config Error: ${err.dataPath.replace('/', '.')} ${err.message}`); 53 | } 54 | 55 | return null; 56 | } 57 | 58 | static _format(errors = []) { 59 | const pathMap = new Map(); 60 | errors.forEach((e) => { 61 | const path = e.dataPath.replace('/', '.'); 62 | const valueArray = pathMap.has(path) 63 | ? pathMap.get(path) 64 | : []; 65 | 66 | pathMap.set(path, [...valueArray, e]); 67 | }); 68 | 69 | const pathes = [...pathMap.keys()].sort(); 70 | const filterPathes = []; 71 | let i = 0; 72 | while (i < pathes.length - 1) { 73 | if (!pathes[i + 1].startsWith(pathes[i])) { 74 | filterPathes.push(pathes[i]); 75 | } 76 | i += 1; 77 | } 78 | filterPathes.push(pathes[i]); 79 | return filterPathes.map((k) => { 80 | const valueArray = pathMap.get(k); 81 | if (valueArray.length === 1) { 82 | return { 83 | dataPath: k, 84 | message: valueArray[0].message, 85 | }; 86 | } 87 | 88 | const types = valueArray 89 | .filter((v) => v.keyword === 'type') 90 | .map((v) => v.params.type) 91 | .join(' or '); 92 | return { 93 | dataPath: k, 94 | message: `should be ${types}`, 95 | }; 96 | }); 97 | } 98 | } 99 | 100 | module.exports = Option; 101 | -------------------------------------------------------------------------------- /packages/svrx/lib/configure/plugin.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const PluginOption = require('./pluginOption'); 3 | const PluginInfo = require('./pluginInfo'); 4 | 5 | const INFO = Symbol('info'); 6 | const OPTION = Symbol('option'); 7 | const CONFIG = Symbol('config'); 8 | const DEFAULTS = Symbol('defaults'); 9 | const BUILTIN_OPTION = Symbol('builtinOption'); 10 | const BUILTIN_DEFAULTS = Symbol('builtinDefaults'); 11 | 12 | const defaults = require('../util/jsonSchemaDefaults'); 13 | 14 | class Plugin { 15 | constructor(data = {}, builtinOption = {}, builtinDefaults = {}) { 16 | this[BUILTIN_OPTION] = builtinOption; 17 | this[BUILTIN_DEFAULTS] = builtinDefaults; 18 | this[INFO] = new PluginInfo(_.omit(data, 'options')); 19 | this[OPTION] = new PluginOption(data.options); 20 | this[CONFIG] = {}; 21 | this[DEFAULTS] = {}; 22 | 23 | this[INFO].validate(); 24 | } 25 | 26 | /** 27 | * check if pathes is builtin query, parse string path to array 28 | * @param pathes 29 | * @returns {void|Array|{pathes: *, isBuiltin: boolean}} 30 | * @private 31 | */ 32 | static _parsePathes(pathes) { 33 | if (_.isString(pathes)) { 34 | pathes = pathes.split('.'); 35 | } 36 | if (_.isArray(pathes) && pathes.length > 0) { 37 | if (pathes[0] === '$') { 38 | return { 39 | isBuiltin: true, 40 | pathes: pathes.slice(1), 41 | }; 42 | } 43 | } 44 | return { 45 | isBuiltin: false, 46 | pathes, 47 | }; 48 | } 49 | 50 | /** 51 | * get plugin option by pathes eg: get('color') 52 | * get builtin option by $.pathes eg: get('$.root') 、get(['$', root', 'rootPath']) 53 | * @param pathes 54 | */ 55 | get(pathes) { 56 | const { isBuiltin, pathes: parsedPathes } = Plugin._parsePathes(pathes); 57 | 58 | if (isBuiltin) { 59 | if (pathes.length === 1) { // get('$') 60 | // get all builtin options and the defaults 61 | return { ...this[BUILTIN_DEFAULTS], ...this[BUILTIN_OPTION].get() }; 62 | } 63 | 64 | // get from builtin option 65 | const userOption = this[BUILTIN_OPTION].get(parsedPathes); 66 | if (userOption === undefined) return _.get(this[BUILTIN_DEFAULTS], parsedPathes); 67 | return userOption; 68 | } 69 | 70 | // get from plugin option 71 | const userOption = this[OPTION].get(parsedPathes); 72 | if (userOption === undefined) return _.get(this[DEFAULTS], parsedPathes); 73 | if (parsedPathes === undefined) { // get all and the defaults 74 | return { ...this[DEFAULTS], ...userOption }; 75 | } 76 | return userOption; 77 | } 78 | 79 | /** 80 | * set plugin option 81 | * @param pluginPathes 82 | * @param value 83 | */ 84 | set(pluginPathes, value) { 85 | this[OPTION].set(pluginPathes, value); 86 | return this; 87 | } 88 | 89 | /** 90 | * get plugin info by pathes 91 | * @param infoPathes 92 | * @returns {*} 93 | */ 94 | getInfo(infoPathes) { 95 | return this[INFO].get(infoPathes); 96 | } 97 | 98 | /** 99 | * set config after plugin loaded 100 | * @param configs 101 | */ 102 | setSchema(configs = {}) { 103 | this[CONFIG] = configs; 104 | this[DEFAULTS] = defaults({ 105 | type: 'object', 106 | properties: configs, 107 | }); 108 | this[OPTION].validate(configs); 109 | } 110 | 111 | getSchema() { 112 | return this[CONFIG]; 113 | } 114 | 115 | watch(pathes, callback) { 116 | const { isBuiltin, pathes: parsedPathes } = Plugin._parsePathes(pathes); 117 | 118 | if (isBuiltin) { 119 | return this[BUILTIN_OPTION].watch(parsedPathes, callback); 120 | } 121 | 122 | return this[OPTION].watch(parsedPathes, callback); 123 | } 124 | 125 | splice(...args) { 126 | this[OPTION].splice(...args); 127 | return this; 128 | } 129 | 130 | del(pathes) { 131 | this[OPTION].del(pathes); 132 | } 133 | } 134 | 135 | module.exports = Plugin; 136 | -------------------------------------------------------------------------------- /packages/svrx/lib/configure/pluginInfo.js: -------------------------------------------------------------------------------- 1 | const { logger } = require('@svrx/util'); 2 | const IModel = require('../model'); 3 | 4 | class PluginInfo extends IModel { 5 | validate() { 6 | if (this.get('name') === undefined) { 7 | logger.error('Plugin name is required'); 8 | process.exit(1); 9 | } 10 | } 11 | } 12 | 13 | module.exports = PluginInfo; 14 | -------------------------------------------------------------------------------- /packages/svrx/lib/configure/pluginOption.js: -------------------------------------------------------------------------------- 1 | const Option = require('./option'); 2 | 3 | class PluginOption extends Option {} 4 | 5 | module.exports = PluginOption; 6 | -------------------------------------------------------------------------------- /packages/svrx/lib/constant.js: -------------------------------------------------------------------------------- 1 | const libPath = require('path'); 2 | 3 | const CORE = 'Core'; 4 | const COMMON = 'Common'; 5 | const GROUPS = { 6 | CORE, 7 | COMMON, 8 | }; 9 | 10 | module.exports = { 11 | PLUGIN_PREFIX: 'svrx-plugin-', 12 | 13 | PRIORITY: { 14 | SERVE: 8, 15 | DEFAULT: 10, 16 | HISTORY_API_FALLBACK: 11, 17 | PROXY: 21, 18 | ROUTER: 22, 19 | MOCK: 30, 20 | TRANSFORM: 100, 21 | INJECTOR: 101, 22 | IO: 200, 23 | CORS: 300, 24 | }, 25 | 26 | ASSET_FIELDS: ['script', 'style'], 27 | // REMOVE POSTFIX like `-beta` in 0.0.1-beta. 28 | /* eslint-disable global-require, import/no-dynamic-require */ 29 | VERSION: require(libPath.join(__dirname, '../package.json')).version.replace(/-.*$/, ''), 30 | BUILTIN_PLUGIN: ['livereload', 'proxy', 'serve', 'cors', 'open', 'history-api-fallback'], 31 | CUSTOM_SCHEMA_TYPES: ['compute', 'function'], 32 | GROUPS, 33 | EVENTS: { 34 | FILE_CHANGE: 'file:change', 35 | CREATE: 'create', 36 | READY: 'ready', 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/svrx/lib/injector/client/index.js: -------------------------------------------------------------------------------- 1 | const events = require('../../shared/events'); 2 | const io = require('../../io/client.js'); 3 | 4 | function getConfig(name) { 5 | return ['get', 'set', 'splice', 'unset'].reduce((api, right) => { 6 | api[right] = (...params) => io.call('$.config', { scope: name, command: right, params }); 7 | return api; 8 | }, {}); 9 | } 10 | 11 | function getScopedInstance(name) { 12 | return { 13 | io, 14 | config: name ? getConfig(name) : getConfig(), 15 | }; 16 | } 17 | 18 | const svrx = { 19 | _getScopedInstance: getScopedInstance, 20 | events: events({}), 21 | io, 22 | }; 23 | 24 | module.exports = svrx; 25 | -------------------------------------------------------------------------------- /packages/svrx/lib/injector/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const replaceStream = require('./replace'); 4 | const { 5 | typeOf, isReadableStream, isHtmlType, isAcceptGzip, 6 | } = require('../util/helper'); 7 | const { PRIORITY } = require('../constant'); 8 | const { gzip } = require('../util/gzip'); 9 | const logger = require('../util/logger'); 10 | 11 | // const 12 | const ASSETS = Symbol('assets'); 13 | const REPLACEMENTS = Symbol('replacements'); 14 | const CLIENT_PATH = path.join(__dirname, 'dist/client.js'); 15 | const BASIC_SCRIPT = fs.readFileSync(CLIENT_PATH, 'utf8'); 16 | const MINE_TYPES = { 17 | style: 'text/css', 18 | script: 'application/javascript', 19 | }; 20 | const TYPE_SPLITS = { 21 | style: '\n', 22 | script: ';\n', 23 | }; 24 | 25 | module.exports = class Injector { 26 | constructor({ config, middleware }) { 27 | this.config = config; 28 | this[ASSETS] = { 29 | style: [], 30 | script: [], 31 | }; 32 | this[REPLACEMENTS] = []; 33 | 34 | middleware.add('$injector', { 35 | priority: PRIORITY.INJECTOR, 36 | onRoute: this.onClient.bind(this), 37 | }); 38 | 39 | middleware.add('$transform', { 40 | priority: PRIORITY.TRANSFORM, 41 | onRoute: this.onTransform.bind(this), 42 | }); 43 | } 44 | 45 | // transform html 46 | 47 | async onTransform(ctx, next) { 48 | await next(); 49 | if (isHtmlType(ctx.response.header) && !ctx._svrx.isInjected) { 50 | ctx.body = this._transform(ctx.body); 51 | ctx._svrx.isInjected = true; 52 | } 53 | } 54 | 55 | // serve /puer/puer-client.js 56 | // serve /puer/puer-client.css 57 | // @TODO 304 Logic 58 | async onClient(ctx, next) { 59 | const { config } = this; 60 | 61 | let match; 62 | ['style', 'script'].some((name) => { 63 | if (ctx.path === config.get(`urls.${name}`)) { 64 | match = name; 65 | return true; 66 | } 67 | return false; 68 | }); 69 | if (match) { 70 | const isGzip = isAcceptGzip(ctx.headers); 71 | ctx.body = await this.getContent(match, ctx); 72 | ctx.set('Content-Type', MINE_TYPES[match]); 73 | if (isGzip) { 74 | ctx.body = await gzip(ctx.body); 75 | ctx.set('Content-Encoding', 'gzip'); 76 | } 77 | } else { 78 | await next(); 79 | } 80 | } 81 | 82 | async getContent(type, ctx) { 83 | const assets = this[ASSETS][type]; 84 | 85 | const appendContent = assets 86 | .filter((m) => !m.test || m.test(ctx.get('Referer'))) 87 | .map((m) => { 88 | let { content } = m; 89 | if (typeof content === 'function') { 90 | content = content(m.config || this.config); 91 | } 92 | return m.filter ? m.filter(content) : content; 93 | }) 94 | .filter((m) => !!m) 95 | .join(TYPE_SPLITS[type] || '\n'); 96 | 97 | const output = type === 'script' ? `${BASIC_SCRIPT}\n${appendContent}` : appendContent; 98 | 99 | return output; 100 | } 101 | 102 | add(type, def) { 103 | const { filename, content } = def; 104 | 105 | if (filename && !content) { 106 | try { 107 | def.content = fs.readFileSync(filename, 'utf8'); 108 | } catch (e) { 109 | logger.error(`readFile(${filename}) failed \n ${e.message}${e.message}`); 110 | return this; 111 | } 112 | } 113 | 114 | this[ASSETS][type].push(def); 115 | return this; 116 | } 117 | 118 | replace(pattern, fn) { 119 | if ( 120 | ['string', 'regexp'].indexOf(typeOf(pattern)) === -1 121 | || ['string', 'function'].indexOf(typeOf(fn)) === -1 122 | ) { 123 | throw Error('invalid replacement'); 124 | } 125 | 126 | this[REPLACEMENTS].push({ 127 | pattern, 128 | fn, 129 | }); 130 | } 131 | 132 | // @TODO FIX split case 133 | _transform(body) { 134 | const { config } = this; 135 | const replaceScript = [ 136 | '', 137 | ``, 138 | ]; 139 | const replaceStyle = [ 140 | /<\/(head|body)>/, 141 | ``, 142 | ]; 143 | 144 | if (body instanceof Buffer) { 145 | body = body.toString('utf8'); 146 | } 147 | if (typeof body === 'string') { 148 | // fix: #120 149 | const lastIndex = body.lastIndexOf(replaceScript[0]); 150 | if (lastIndex > 0) { 151 | body = body.slice(0, lastIndex) 152 | + replaceScript[1] 153 | + body.slice(replaceScript[0].length + lastIndex); 154 | } 155 | return this._replace(body.replace(...replaceStyle), 'string'); 156 | } 157 | if (isReadableStream(body)) { 158 | return this._replace( 159 | body.pipe(replaceStream(...replaceScript)).pipe(replaceStream(...replaceStyle)), 160 | 'stream', 161 | ); 162 | } 163 | 164 | return body; 165 | } 166 | 167 | _replace(body, type) { 168 | if (type === 'string') { 169 | return this[REPLACEMENTS].reduce((occur, item) => occur.replace(item.pattern, item.fn), body); 170 | } 171 | return this[REPLACEMENTS].reduce( 172 | (occur, item) => occur.pipe(replaceStream(item.pattern, item.fn)), 173 | body, 174 | ); 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /packages/svrx/lib/injector/replace.js: -------------------------------------------------------------------------------- 1 | /** 2 | * stream#replace 3 | * 4 | */ 5 | 6 | const { Transform } = require('stream'); 7 | 8 | /** 9 | * str.replace (stream version) 10 | * rs.pipe(replaceStream('', '