├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── release.yml │ ├── test.yml │ └── validate.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── index.test.js └── lib │ └── utils.js ├── commitlint.config.js ├── docs └── configure.md ├── example ├── serverless.yml └── sls.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── release.config.js ├── serverless.component.yml └── src ├── _shims └── sl_handler.js ├── config.js ├── package.json ├── serverless.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_size = 2 11 | indent_style = space 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | example 5 | *.test.js 6 | src/_src 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['prettier'], 4 | plugins: ['import', 'prettier'], 5 | env: { 6 | es6: true, 7 | jest: true, 8 | node: true 9 | }, 10 | parser: 'babel-eslint', 11 | parserOptions: { 12 | ecmaVersion: 2018, 13 | sourceType: 'module', 14 | ecmaFeatures: { 15 | jsx: true 16 | } 17 | }, 18 | globals: { 19 | on: true // for the Socket file 20 | }, 21 | rules: { 22 | 'array-bracket-spacing': [ 23 | 'error', 24 | 'never', 25 | { 26 | objectsInArrays: false, 27 | arraysInArrays: false 28 | } 29 | ], 30 | 'arrow-parens': ['error', 'always'], 31 | 'arrow-spacing': ['error', { before: true, after: true }], 32 | 'comma-dangle': ['error', 'never'], 33 | curly: 'error', 34 | 'eol-last': 'error', 35 | 'func-names': 'off', 36 | 'id-length': [ 37 | 'error', 38 | { 39 | min: 1, 40 | max: 50, 41 | properties: 'never', 42 | exceptions: ['e', 'i', 'n', 't', 'x', 'y', 'z', '_', '$'] 43 | } 44 | ], 45 | 'no-alert': 'error', 46 | 'no-console': 'off', 47 | 'no-const-assign': 'error', 48 | 'no-else-return': 'error', 49 | 'no-empty': 'off', 50 | 'no-shadow': 'error', 51 | 'no-undef': 'error', 52 | 'no-unused-vars': 'error', 53 | 'no-use-before-define': 'error', 54 | 'no-useless-constructor': 'error', 55 | 'object-curly-newline': 'off', 56 | 'object-shorthand': 'off', 57 | 'prefer-const': 'error', 58 | 'prefer-destructuring': ['error', { object: true, array: false }], 59 | quotes: [ 60 | 'error', 61 | 'single', 62 | { 63 | allowTemplateLiterals: true, 64 | avoidEscape: true 65 | } 66 | ], 67 | semi: ['error', 'never'], 68 | 'spaced-comment': 'error', 69 | strict: ['error', 'global'], 70 | 'prettier/prettier': 'error' 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | env: 12 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | with: 17 | persist-credentials: false 18 | 19 | - name: Install Node.js and npm 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 14.x 23 | registry-url: https://registry.npmjs.org 24 | 25 | - name: Retrieve dependencies from cache 26 | id: cacheNpm 27 | uses: actions/cache@v2 28 | with: 29 | path: | 30 | ~/.npm 31 | node_modules 32 | key: npm-v14-${{ runner.os }}-refs/heads/master-${{ hashFiles('package.json') }} 33 | restore-keys: npm-v14-${{ runner.os }}-refs/heads/master- 34 | 35 | - name: Install dependencies 36 | if: steps.cacheNpm.outputs.cache-hit != 'true' 37 | run: | 38 | npm update --no-save 39 | npm update --save-dev --no-save 40 | - name: Releasing 41 | run: | 42 | npm run release 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 45 | GIT_AUTHOR_NAME: slsplus 46 | GIT_AUTHOR_EMAIL: slsplus.sz@gmail.com 47 | GIT_COMMITTER_NAME: slsplus 48 | GIT_COMMITTER_EMAIL: slsplus.sz@gmail.com 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | with: 15 | # Ensure connection with 'master' branch 16 | fetch-depth: 2 17 | 18 | - name: Install Node.js and npm 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14.x 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Retrieve dependencies from cache 25 | id: cacheNpm 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.npm 30 | node_modules 31 | key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 32 | restore-keys: | 33 | npm-v14-${{ runner.os }}-${{ github.ref }}- 34 | npm-v14-${{ runner.os }}-refs/heads/master- 35 | 36 | - name: Install dependencies 37 | if: steps.cacheNpm.outputs.cache-hit != 'true' 38 | run: | 39 | npm update --no-save 40 | npm update --save-dev --no-save 41 | - name: Running tests 42 | run: npm run test 43 | env: 44 | SERVERLESS_PLATFORM_VENDOR: tencent 45 | GLOBAL_ACCELERATOR_NA: true 46 | TENCENT_APP_ID: ${{ secrets.TENCENT_APP_ID }} 47 | TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} 48 | TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} 49 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | lintAndFormatting: 9 | name: Lint & Formatting 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | with: 15 | # Ensure connection with 'master' branch 16 | fetch-depth: 2 17 | 18 | - name: Install Node.js and npm 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 14.x 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Retrieve dependencies from cache 25 | id: cacheNpm 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.npm 30 | node_modules 31 | key: npm-v14-${{ runner.os }}-${{ github.ref }}-${{ hashFiles('package.json') }} 32 | restore-keys: | 33 | npm-v14-${{ runner.os }}-${{ github.ref }}- 34 | npm-v14-${{ runner.os }}-refs/heads/master- 35 | 36 | - name: Install dependencies 37 | if: steps.cacheNpm.outputs.cache-hit != 'true' 38 | run: | 39 | npm update --no-save 40 | npm update --save-dev --no-save 41 | 42 | - name: Validate Formatting 43 | run: npm run prettier:fix 44 | - name: Validate Lint rules 45 | run: npm run lint:fix 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | *.log 5 | .serverless 6 | v8-compile-cache-* 7 | jest/* 8 | coverage 9 | .serverless_plugins 10 | testProjects/*/package-lock.json 11 | testProjects/*/yarn.lock 12 | .serverlessUnzipped 13 | node_modules 14 | .vscode/ 15 | .eslintcache 16 | dist 17 | .idea 18 | build/ 19 | .env* 20 | env.js 21 | package-lock.json 22 | test 23 | yarn.lock -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | CHANGELOG.md 5 | *.test.js 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.5.0](https://github.com/serverless-components/tencent-websocket/compare/v0.4.1...v0.5.0) (2021-03-19) 2 | 3 | 4 | ### Features 5 | 6 | * optimize deploy flow and update deps ([#16](https://github.com/serverless-components/tencent-websocket/issues/16)) ([8b74d8c](https://github.com/serverless-components/tencent-websocket/commit/8b74d8cb267b0ef23e3f3f1f071bfd4fc82978a2)) 7 | 8 | ## [0.4.1](https://github.com/serverless-components/tencent-websocket/compare/v0.4.0...v0.4.1) (2020-12-15) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * update deploy and remove flow ([#14](https://github.com/serverless-components/tencent-websocket/issues/14)) ([613200b](https://github.com/serverless-components/tencent-websocket/commit/613200be6fff00b90c87f2d97d1452e5f8b5582a)) 14 | 15 | # [0.4.0](https://github.com/serverless-components/tencent-websocket/compare/v0.3.5...v0.4.0) (2020-11-25) 16 | 17 | 18 | ### Features 19 | 20 | * uniform configure ([#13](https://github.com/serverless-components/tencent-websocket/issues/13)) ([70f438a](https://github.com/serverless-components/tencent-websocket/commit/70f438ac54f104baf68930b38f3ac44ca21db6b9)) 21 | 22 | ## [0.3.5](https://github.com/serverless-components/tencent-websocket/compare/v0.3.4...v0.3.5) (2020-11-06) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * use yamlToSdkInputs to transfer input for sdk ([9d07376](https://github.com/serverless-components/tencent-websocket/commit/9d073761c06bb882bcaf01c7a8cd8782d6f6d1ea)) 28 | 29 | ## [0.3.4](https://github.com/serverless-components/tencent-websocket/compare/v0.3.3...v0.3.4) (2020-11-02) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * optimize config ([0fdc055](https://github.com/serverless-components/tencent-websocket/commit/0fdc05559ec6cc38ddbbf76183e24c8ef82b9034)) 35 | 36 | ## [0.3.3](https://github.com/serverless-components/tencent-websocket/compare/v0.3.2...v0.3.3) (2020-10-28) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * async timeout ([f1d69a0](https://github.com/serverless-components/tencent-websocket/commit/f1d69a00578630ed3f7979a9933d8f3363d49523)) 42 | 43 | ## [0.3.2](https://github.com/serverless-components/tencent-websocket/compare/v0.3.1...v0.3.2) (2020-10-26) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * faas name config ([36c0ac4](https://github.com/serverless-components/tencent-websocket/commit/36c0ac48ab363d193b080a01faeb4c66c7323194)) 49 | * faas remove name bug ([db151fc](https://github.com/serverless-components/tencent-websocket/commit/db151fc19c5535e39ce7ce3f058f7fba05330c17)) 50 | 51 | ## [0.3.1](https://github.com/serverless-components/tencent-websocket/compare/v0.3.0...v0.3.1) (2020-10-26) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * handler config and outputs format ([#5](https://github.com/serverless-components/tencent-websocket/issues/5)) ([311c47b](https://github.com/serverless-components/tencent-websocket/commit/311c47b84d5dfce7d90d8c46c07e9c66b8706917)) 57 | 58 | # [0.3.0](https://github.com/serverless-components/tencent-websocket/compare/v0.2.3...v0.3.0) (2020-10-25) 59 | 60 | 61 | ### Features 62 | 63 | * complete version 2 ([f0e79f4](https://github.com/serverless-components/tencent-websocket/commit/f0e79f43ec9017cc2e8958c959973e01b49f48ab)) 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tencent Cloud, Inc. 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 | [![Serverless Websocket Tencent Cloud](https://img.serverlesscloud.cn/2020210/1581351457765-WebSocket_%E9%95%BF.png)](http://serverless.com) 2 | 3 | # 腾讯云 Websocket Serverless Component 4 | 5 | 腾讯云 Websocket Serverless Component。 6 | 7 | 快速开始: 8 | 9 | 1. [**安装**](#1-安装) 10 | 2. [**创建**](#2-创建) 11 | 3. [**部署**](#3-部署) 12 | 4. [**配置**](#4-配置) 13 | 5. [**开发调试**](#5-开发调试) 14 | 6. [**查看状态**](#6-查看状态) 15 | 7. [**移除**](#7-移除) 16 | 17 | 更多资源: 18 | 19 | - [**架构说明**](#架构说明) 20 | - [**账号配置**](#账号配置) 21 | 22 | ### 1. 安装 23 | 24 | 通过 npm 安装最新版本的 Serverless Framework 25 | 26 | ```bash 27 | $ npm install -g serverless 28 | ``` 29 | 30 | ### 2. 创建 31 | 32 | ```bash 33 | $ sls init websocket-starter --name example 34 | $ cd example 35 | ``` 36 | 37 | ### 3. 部署 38 | 39 | 在 `serverless.yml` 文件所在的项目根目录,运行以下指令进行部署: 40 | 41 | ```bash 42 | $ serverless deploy 43 | ``` 44 | 45 | 部署时需要进行身份验证,如您的账号未 [登陆](https://cloud.tencent.com/login) 或 [注册](https://cloud.tencent.com/register) 腾讯云,您可以直接通过 `微信` 扫描命令行中的二维码进行授权登陆和注册。 46 | 47 | > 注意: 如果希望查看更多部署过程的信息,可以通过`serverless deploy --debug` 命令查看部署过程中的实时日志信息。 48 | 49 | 部署成功后,可以通过控制台输出的 `apigw.url` (类似 ws 开头的链接) 进行 websocket 连接。 50 | 51 | ### 4. 配置 52 | 53 | 在项目根目录,创建 `serverless.yml` 文件,在其中进行如下配置 54 | 55 | ```bash 56 | $ touch serverless.yml 57 | ``` 58 | 59 | ```yml 60 | # serverless.yml 61 | 62 | app: appDemo 63 | stage: dev 64 | 65 | component: websocket 66 | name: websocketDemo 67 | 68 | inputs: 69 | region: ap-guangzhou 70 | src: 71 | src: ./ 72 | exclude: 73 | - .env 74 | faas: 75 | name: websocket-function 76 | timeout: 10 77 | apigw: 78 | timeout: 30 79 | environment: release 80 | protocols: 81 | - https 82 | ``` 83 | 84 | 点此查看[全量配置及配置说明](https://github.com/serverless-components/tencent-websocket/tree/master/docs/configure.md) 85 | 86 | 当你根据该配置文件更新配置字段后,再次运行 `serverless deploy` 或者 `serverless` 就可以更新配置到云端。 87 | 88 | ### 5. 开发调试 89 | 90 | 部署了 websocket 应用后,可以通过开发调试能力对该项目进行二次开发,从而开发一个生产应用。在本地修改和更新代码后,不需要每次都运行 `serverless deploy` 命令来反复部署。你可以直接通过 `serverless dev` 命令对本地代码的改动进行检测和自动上传。 91 | 92 | 可以通过在 `serverless.yml`文件所在的目录下运行 `serverless dev` 命令开启开发调试能力。 93 | 94 | `serverless dev` 同时支持实时输出云端日志,每次部署完毕后,对项目进行访问,即可在命令行中实时输出调用日志,便于查看业务情况和排障。 95 | 96 | 除了实时日志输出之外,针对 Node.js 应用,当前也支持云端调试能力。在开启 `serverless dev` 命令之后,将会自动监听远端端口,并将函数的超时时间临时配置为 900s。此时你可以通过访问 chrome://inspect/#devices 查找远端的调试路径,并直接对云端代码进行断点等调试。在调试模式结束后,需要再次部署从而将代码更新并将超时时间设置为原来的值。详情参考[开发模式和云端调试](https://cloud.tencent.com/document/product/1154/43220)。 97 | 98 | ### 6. 查看状态 99 | 100 | 在`serverless.yml`文件所在的目录下,通过如下命令查看部署状态: 101 | 102 | ``` 103 | $ serverless info 104 | ``` 105 | 106 | ### 7. 移除 107 | 108 | 在`serverless.yml`文件所在的目录下,通过以下命令移除部署的 Websocket 服务。移除后该组件会对应删除云上部署时所创建的所有相关资源。 109 | 110 | ``` 111 | $ serverless remove 112 | ``` 113 | 114 | 和部署类似,支持通过 `serverless remove --debug` 命令查看移除过程中的实时日志信息。 115 | 116 | ## 架构说明 117 | 118 | Websocket 组件将在腾讯云账户中使用到如下 Serverless 服务: 119 | 120 | - [x] **API 网关** - API 网关将会接收外部请求并且转发到 SCF 云函数中。 121 | - [x] **SCF 云函数** - 云函数将承载 Websocket 应用。 122 | - [x] **CAM 访问控制** - 该组件会创建默认 CAM 角色用于授权访问关联资源。 123 | - [x] **COS 对象存储** - 为确保上传速度和质量,云函数压缩并上传代码时,会默认将代码包存储在特定命名的 COS 桶中。 124 | - [x] **SSL 证书服务** - 如果你在 yaml 文件中配置了 `apigw.customDomains` 字段,需要做自定义域名绑定并开启 HTTPS 时,也会用到证书管理服务和域名服务。Serverless Framework 会根据已经备案的域名自动申请并配置 SSL 证书。 125 | 126 | ## 账号配置(可选) 127 | 128 | 当前默认支持 CLI 扫描二维码登录,如您希望配置持久的环境变量/秘钥信息,也可以本地创建 `.env` 文件 129 | 130 | 在 `.env` 文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存 131 | 132 | 如果没有腾讯云账号,可以在此 [注册新账号](https://cloud.tencent.com/register)。 133 | 134 | 如果已有腾讯云账号,可以在 [API 密钥管理](https://console.cloud.tencent.com/cam/capi) 中获取 `SecretId` 和`SecretKey`. 135 | 136 | ```text 137 | # .env 138 | TENCENT_SECRET_ID=123 139 | TENCENT_SECRET_KEY=123 140 | ``` 141 | 142 | ## License 143 | 144 | MIT License 145 | 146 | Copyright (c) 2020 Tencent Cloud, Inc. 147 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { generateId, getServerlessSdk } = require('./lib/utils') 3 | 4 | // set enough timeout for deployment to finish 5 | jest.setTimeout(300000) 6 | 7 | // the yaml file we're testing against 8 | const instanceYaml = { 9 | org: 'orgDemo', 10 | app: 'appDemo', 11 | component: 'websocket@dev', 12 | name: `websocket-integration-tests-${generateId()}`, 13 | stage: 'dev', 14 | inputs: { 15 | src: { 16 | src: path.join(__dirname, '..', 'example'), 17 | exclude: [ '.env' ], 18 | }, 19 | region: 'ap-guangzhou', 20 | faas: { name: `websocket-test`, runtime: 'Nodejs10.15' }, 21 | apigw: { environment: 'test' } 22 | } 23 | } 24 | 25 | const credentials = { 26 | tencent: { 27 | SecretId: process.env.TENCENT_SECRET_ID, 28 | SecretKey: process.env.TENCENT_SECRET_KEY, 29 | } 30 | } 31 | 32 | // get serverless construct sdk 33 | const sdk = getServerlessSdk(instanceYaml.org, process.env.TENCENT_APP_ID) 34 | 35 | it('should deploy success', async () => { 36 | const instance = await sdk.deploy(instanceYaml, credentials) 37 | 38 | expect(instance).toBeDefined() 39 | expect(instance.instanceName).toEqual(instanceYaml.name) 40 | // get src from template by default 41 | expect(instance.outputs.region).toEqual(instanceYaml.inputs.region) 42 | expect(instance.outputs.apigw).toBeDefined() 43 | expect(instance.outputs.apigw.environment).toEqual(instanceYaml.inputs.apigw.environment) 44 | expect(instance.outputs.faas).toBeDefined() 45 | expect(instance.outputs.faas.name).toEqual(instanceYaml.inputs.faas.name) 46 | expect(instance.outputs.faas.runtime).toEqual(instanceYaml.inputs.faas.runtime) 47 | }) 48 | 49 | it('should remove success', async () => { 50 | await sdk.remove(instanceYaml, credentials) 51 | result = await sdk.getInstance(instanceYaml.org, instanceYaml.stage, instanceYaml.app, instanceYaml.name) 52 | 53 | expect(result.instance.instanceStatus).toEqual('inactive') 54 | }) 55 | -------------------------------------------------------------------------------- /__tests__/lib/utils.js: -------------------------------------------------------------------------------- 1 | const { ServerlessSDK } = require('@serverless/platform-client-china') 2 | 3 | /* 4 | * Generate random id 5 | */ 6 | const generateId = () => 7 | Math.random() 8 | .toString(36) 9 | .substring(6) 10 | 11 | /* 12 | * Initializes and returns an instance of the serverless sdk 13 | * @param ${string} orgName - the serverless org name. 14 | */ 15 | const getServerlessSdk = (orgName, orgUid) => { 16 | const sdk = new ServerlessSDK({ 17 | context: { 18 | orgUid, 19 | orgName 20 | } 21 | }) 22 | return sdk 23 | } 24 | 25 | module.exports = { generateId, getServerlessSdk } 26 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | const Configuration = { 2 | /* 3 | * Resolve and load @commitlint/config-conventional from node_modules. 4 | * Referenced packages must be installed 5 | */ 6 | extends: ['@commitlint/config-conventional'] 7 | } 8 | 9 | module.exports = Configuration 10 | -------------------------------------------------------------------------------- /docs/configure.md: -------------------------------------------------------------------------------- 1 | # 配置文档 2 | 3 | ## 全部配置 4 | 5 | ```yml 6 | # serverless.yml 7 | 8 | org: orgDemo # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid,必须为字符串 9 | app: appDemo # (可选) 用于记录组织信息. 默认与name相同,必须为字符串 10 | stage: dev # (可选) 用于区分环境信息,默认值是 dev 11 | 12 | component: websocket # (必选) 组件名称,在该实例中为websocket 13 | name: websocketDemo # 必选) 组件实例名称. 14 | 15 | inputs: 16 | region: ap-guangzhou # 云函数所在区域 17 | entryFile: sls.js # 自定义 server 的入口文件名,默认为 sls.js,如果不想修改文件名为 sls.js 可以自定义 18 | src: 19 | src: ./ # 本地需要打包的文件目录 20 | exclude: # 被排除的文件或目录 21 | - .env 22 | # - 'node_modules/**' 23 | faas: # 函数配置相关 24 | name: websocketDemo # 云函数名称 25 | runtime: Nodejs10.15 # 运行环境 26 | timeout: 10 # 超时时间,单位秒 27 | eip: false # 是否固定出口IP 28 | memorySize: 128 # 内存大小,单位MB 29 | environments: # 环境变量 30 | - key: TEST 31 | value: 123 32 | vpc: # 私有网络配置 33 | vpcId: vpc-xxxx # 私有网络的Id 34 | subnetId: subnet-xxxx # 子网ID 35 | layers: 36 | - name: layerName # layer名称 37 | version: 1 # 版本 38 | apigw: # api网关配置 39 | isDisabled: false # 是否禁用自动创建 API 网关功能 40 | id: service-xxx # api网关服务ID 41 | name: mytest # api网关服务名称 42 | description: mytest # api网关描述 43 | cors: true # 允许跨域 44 | protocols: 45 | - http 46 | - https 47 | environment: test 48 | timeout: 15 49 | customDomains: # 自定义域名绑定 50 | - domain: abc.com # 待绑定的自定义的域名 51 | certId: abcdefg # 待绑定自定义域名的证书唯一 ID 52 | customMap: true # 是否自定义路径 53 | pathMap: 54 | - path: / 55 | environment: release 56 | protocols: # 绑定自定义域名的协议类型,默认与服务的前端协议一致。 57 | - http 58 | - https 59 | ``` 60 | 61 | ## 配置描述 62 | 63 | 主要的参数 64 | 65 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 66 | | --------- | :--: | :-------------------------: | :------------: | :------------------------- | 67 | | src | 是 | [Src](#Src) 或者 string | | | 代码目录, 如果是对象 | 68 | | region | 否 | | `ap-guangzhou` | 项目部署所在区域 | 69 | | entryFile | 否 | | `sls.js` | 自定义 server 的入口文件名 | 70 | | faas | 否 | [FaasConfig](#FaasConfig) | | 函数配置 | 71 | | apigw | 否 | [ApigwConfig](#ApigwConfig) | | API 网关配置 | 72 | 73 | ## Src 74 | 75 | 执行目录 76 | 77 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 78 | | -------- | :--: | :------: | :----: | :----------------------------------------- | 79 | | src | 否 | string | | 代码路径。与 obejct 不能同时存在。 | 80 | | exclude | 否 | string[] | | 不包含的文件或路径, 遵守 [glob 语法][glob] | 81 | | bucket | 否 | string | | bucket 名称。 | 82 | | obejct | 否 | string | | 部署的代码在存储桶中的路径。 | 83 | 84 | > **注意**:如果配置了 src,表示部署 src 的代码并压缩成 zip 后上传到 bucket-appid 对应的存储桶中;如果配置了 obejct,表示获取 bucket-appid 对应存储桶中 obejct 对应的代码进行部署。 85 | 86 | 比如需要忽略项目的 `node_modules` 目录,可以配置如下: 87 | 88 | ```yaml 89 | exclude: 90 | - 'node_modules/**' 91 | ``` 92 | 93 | ### FaasConfig 94 | 95 | 函数配置,参考: https://cloud.tencent.com/document/product/583/18586 96 | 97 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 98 | | ------------ | :--: | :---------------: | :-----------: | :------------------------------------------------------------------ | 99 | | runtime | 否 | string | `Nodejs10.15` | 执行环境, 目前支持: Nodejs6.10, Nodejs8.9, Nodejs10.15, Nodejs12.16 | 100 | | name | 否 | string | | 云函数名称 | 101 | | timeout | 否 | number | `3` | 函数最长执行时间,单位为秒,可选值范围 1-900 秒,默认为 3 秒 | 102 | | memorySize | 否 | number | `128` | 函数运行时内存大小,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | 103 | | environments | 否 | [Env](#Env)[] | | 函数的环境变量 | 104 | | vpc | 否 | [Vpc](#Vpc) | | 函数的 VPC 配置 | 105 | | eip | 否 | boolean | `false` | 是否固定出口 IP | 106 | | layers | 否 | [Layer](#Layer)[] | | 云函数绑定的 layer | 107 | 108 | > 此处只是列举,`faas` 参数支持 [scf][scf-config] 组件的所有基础配置( `events` 除外) 109 | 110 | ##### Env 111 | 112 | 环境变量 113 | 114 | | 参数名称 | 类型 | 描述 | 115 | | ------------ | -------------- | :----------- | 116 | | key | string | 环境变量 key | 117 | | value string | 环境变量 value | 118 | 119 | ##### Vpc 120 | 121 | VPC - 私有网络 122 | 123 | | 参数名称 | 类型 | 描述 | 124 | | -------- | ------ | :------ | 125 | | vpcId | string | VPC ID | 126 | | subnetId | string | 子网 ID | 127 | 128 | ##### Layer 129 | 130 | 层配置 131 | 132 | | 参数名称 | 类型 | 描述 | 133 | | -------- | ------ | :----- | 134 | | name | string | 层名称 | 135 | | version | string | 层版本 | 136 | 137 | ### ApigwConfig 138 | 139 | API 网关配置 140 | 141 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 142 | | ------------- | :--: | :------------------------------ | :----------- | :--------------------------------------------------------------- | 143 | | id | 否 | | | API 网关服务 ID,如果存在将使用这个 API 网关服务 | 144 | | name | 否 | | `serverless` | API 网关服务名称, 默认创建一个新的服务名称 | 145 | | description | 否 | | | API 网关服务描述 | 146 | | protocols | 否 | string[] | `['http']` | 前端请求的类型,如 http,https,http 与 https | 147 | | environment | 否 | string | `release` | 发布环境. 目前支持三种发布环境: test、prepub、release. | 148 | | cors | 否 | boolean | `false` | 开启跨域。默认值为否。 | 149 | | timeout | 否 | number | `15` | Api 超时时间,单位: 秒 | 150 | | isDisabled | 否 | boolean | `false` | 关闭自动创建 API 网关功能。默认值为否,即默认自动创建 API 网关。 | 151 | | customDomains | 否 | [CustomDomain](#CustomDomain)[] | | 自定义 API 域名配置 | 152 | 153 | ##### CustomDomain 154 | 155 | 自定义域名配置,相关文档: https://cloud.tencent.com/document/product/628/14906 156 | 157 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 158 | | --------- | :--: | :-------------------: | :-----: | :-------------------------------------------------------------------------- | 159 | | domain | 是 | string | | 待绑定的自定义的域名。 | 160 | | certId | 否 | string | | 待绑定自定义域名的证书唯一 ID,如果设置了 type 为 `https`,则为必选 | 161 | | customMap | 否 | string | `false` | 是否自定义路径映射。为 `true` 时,表示自定义路径映射,此时 `pathMap` 必填。 | 162 | | pathMap | 否 | [PathMap](#PathMap)[] | `[]` | 自定义路径映射的路径。 | 163 | | protocol | 否 | string[] | | 绑定自定义域名的协议类型,默认与服务的前端协议一致。 | 164 | 165 | #### PathMap 166 | 167 | 自定义路径映射 168 | 169 | | 参数名称 | 必选 | 类型 | Description | 170 | | ----------- | :--: | :----- | :------------- | 171 | | path | 是 | string | 自定义映射路径 | 172 | | environment | 是 | string | 自定义映射环境 | 173 | 174 | > 使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 175 | 176 | ###### AclConfig 177 | 178 | COS 访问控制 179 | 180 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 181 | | ----------- | :--: | :----: | :-----------: | :----------- | 182 | | permissions | 是 | string | `public-read` | 公共权限配置 | 183 | 184 | ###### SourceConfig 185 | 186 | 静态资源目录配置,可以根据个人需要自定义需要托管到 COS 的静态资源目录 187 | 188 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 189 | | -------- | :--: | :----: | :----: | :------- | 190 | | src | 是 | string | | 源目录 | 191 | | target | 是 | string | | 目标目录 | 192 | 193 | ##### CdnConfig 194 | 195 | CDN 配置 196 | 197 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 198 | | ------------- | :--: | :-----------------------------: | :--------: | :--------------------------------------------------------- | 199 | | domain | 是 | string | | CDN 域名 | 200 | | area | 否 | string | `mainland` | 加速区域,mainland: 大陆,overseas:海外,global:全球加速 | 201 | | autoRefresh | 否 | boolean | `true` | 是否自动刷新 CDN | 202 | | refreshType | 否 | boolean | `delete` | CDN 刷新类型,delete:刷新全部资源,flush:刷新变更资源 | 203 | | https | 否 | [Https](#Https) | | https 配置 | 204 | | forceRedirect | 否 | [ForceRedirect](#ForceRedirect) | | 访问协议强制跳转配置 | 205 | 206 | ###### Https 207 | 208 | HTTPS 相关配置 209 | 210 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 211 | | -------- | :--: | :----: | :----: | :------------------------------------ | 212 | | certId | 是 | string | | 腾讯云托管域名证书 ID | 213 | | http2 | 是 | string | | 是否开启 HTTP2,on: 开启,off: 关闭 | 214 | 215 | ###### ForceRedirect 216 | 217 | 访问协议强制跳转配置 218 | 219 | | 参数名称 | 必选 | 类型 | 默认值 | 描述 | 220 | | ------------------ | :--: | :----: | :----: | :------------------------------------------------------------- | 221 | | switch | 是 | string | `on` | 访问强制跳转配置开关, on:开启,off:关闭 | 222 | | redirectType | 是 | string | `http` | 访问强制跳转类型,http:强制 http 跳转,https:强制 https 跳转 | 223 | | redirectStatusCode | 是 | number | `301` | 强制跳转时返回状态码,支持 301、302 | 224 | 225 | 226 | 227 | [glob]: https://github.com/isaacs/node-glob 228 | [scf-config]: https://github.com/serverless-components/tencent-scf/tree/master/docs/configure.md 229 | -------------------------------------------------------------------------------- /example/serverless.yml: -------------------------------------------------------------------------------- 1 | app: appDemo 2 | stage: dev 3 | 4 | component: websocket 5 | name: websocketDemo 6 | 7 | inputs: 8 | region: ap-guangzhou 9 | src: 10 | src: ./ 11 | exclude: 12 | - .env 13 | faas: 14 | name: websocket-function 15 | timeout: 10 16 | apigw: 17 | timeout: 30 18 | environment: release 19 | protocols: 20 | - https 21 | -------------------------------------------------------------------------------- /example/sls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Connect 3 | */ 4 | on('connect', async (data, socket) => { 5 | console.log(`connect ${socket.id}`) 6 | console.log('data', data) 7 | return 'connected' 8 | }) 9 | 10 | /** 11 | * Disconnect 12 | */ 13 | on('disconnect', async (data, socket) => { 14 | console.log(`disconnect ${socket.id}`) 15 | console.log('data', data) 16 | return 'closed' 17 | }) 18 | 19 | /** 20 | * Message 21 | */ 22 | on('message', async (data, socket) => { 23 | console.log('message', socket, data) 24 | console.log('sending to: ', socket.id) 25 | await socket.send( 26 | JSON.stringify({ status: 'sending data', data: data || 'hello websocket' }), 27 | socket.id 28 | ) 29 | }) 30 | 31 | /** 32 | * Default 33 | */ 34 | on('default', async (data, socket) => { 35 | console.log('message', socket, data) 36 | console.log('sending to: ', socket.id) 37 | await socket.send( 38 | JSON.stringify({ status: 'sending default data', data: data || 'hello websocket' }), 39 | socket.id 40 | ) 41 | }) 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | require('dotenv').config({ path: join(__dirname, '.env.test') }) 3 | 4 | const config = { 5 | verbose: true, 6 | silent: false, 7 | testTimeout: 600000, 8 | testEnvironment: 'node', 9 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$', 10 | testPathIgnorePatterns: ['/node_modules/', '/__tests__/lib/'], 11 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 12 | } 13 | 14 | module.exports = config 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless/websocket", 3 | "main": "src/serverless.js", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Tencent Cloud Websocket Serverless Component", 8 | "scripts": { 9 | "test": "jest", 10 | "commitlint": "commitlint -f HEAD@{15}", 11 | "lint": "eslint --ext .js,.ts,.tsx .", 12 | "lint:fix": "eslint --fix --ext .js,.ts,.tsx .", 13 | "prettier": "prettier --check '**/*.{css,html,js,json,md,yaml,yml}'", 14 | "prettier:fix": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'", 15 | "release": "semantic-release", 16 | "release-local": "node -r dotenv/config node_modules/semantic-release/bin/semantic-release --no-ci --dry-run", 17 | "check-dependencies": "npx npm-check --skip-unused --update" 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "ygsec && lint-staged", 22 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 23 | "pre-push": "ygsec && npm run lint:fix && npm run prettier:fix" 24 | } 25 | }, 26 | "lint-staged": { 27 | "**/*.{js,ts,tsx}": [ 28 | "npm run lint:fix", 29 | "git add ." 30 | ], 31 | "**/*.{css,html,js,json,md,yaml,yml}": [ 32 | "npm run prettier:fix", 33 | "git add ." 34 | ] 35 | }, 36 | "author": "Tencent Cloud, Inc.", 37 | "license": "MIT", 38 | "dependencies": {}, 39 | "devDependencies": { 40 | "@commitlint/cli": "^8.3.5", 41 | "@commitlint/config-conventional": "^8.3.4", 42 | "@semantic-release/changelog": "^5.0.0", 43 | "@semantic-release/commit-analyzer": "^8.0.1", 44 | "@semantic-release/git": "^9.0.0", 45 | "@semantic-release/release-notes-generator": "^9.0.1", 46 | "@serverless/platform-client-china": "^2.1.9", 47 | "@ygkit/secure": "^0.0.3", 48 | "babel-eslint": "^10.1.0", 49 | "dotenv": "^8.2.0", 50 | "eslint": "^6.8.0", 51 | "eslint-config-prettier": "^6.10.0", 52 | "eslint-plugin-import": "^2.20.1", 53 | "eslint-plugin-prettier": "^3.1.2", 54 | "husky": "^4.2.3", 55 | "jest": "^26.6.1", 56 | "lint-staged": "^10.0.8", 57 | "prettier": "^1.19.1", 58 | "semantic-release": "^17.0.4" 59 | }, 60 | "directories": { 61 | "doc": "docs", 62 | "example": "example" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "git+https://github.com/serverless-components/tencent-websocket.git" 67 | }, 68 | "keywords": [ 69 | "serverless-websocket", 70 | "websocket", 71 | "serverless", 72 | "serverless-framework", 73 | "serverless-components", 74 | "tencent-cloud" 75 | ], 76 | "bugs": { 77 | "url": "https://github.com/serverless-components/tencent-websocket/issues" 78 | }, 79 | "homepage": "https://github.com/serverless-components/tencent-websocket#readme" 80 | } 81 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | printWidth: 100, 4 | semi: false, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'none' 8 | } 9 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verifyConditions: [ 3 | '@semantic-release/changelog', 4 | '@semantic-release/git', 5 | '@semantic-release/github' 6 | ], 7 | plugins: [ 8 | [ 9 | '@semantic-release/commit-analyzer', 10 | { 11 | preset: 'angular', 12 | parserOpts: { 13 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'] 14 | } 15 | } 16 | ], 17 | [ 18 | '@semantic-release/release-notes-generator', 19 | { 20 | preset: 'angular', 21 | parserOpts: { 22 | noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING'] 23 | }, 24 | writerOpts: { 25 | commitsSort: ['subject', 'scope'] 26 | } 27 | } 28 | ], 29 | [ 30 | '@semantic-release/changelog', 31 | { 32 | changelogFile: 'CHANGELOG.md' 33 | } 34 | ], 35 | [ 36 | '@semantic-release/git', 37 | { 38 | assets: ['package.json', 'src/**', 'CHANGELOG.md'], 39 | message: 'chore(release): version ${nextRelease.version} \n\n${nextRelease.notes}' 40 | } 41 | ], 42 | [ 43 | '@semantic-release/github', 44 | { 45 | assets: ['!.env'] 46 | } 47 | ] 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /serverless.component.yml: -------------------------------------------------------------------------------- 1 | name: websocket 2 | version: 0.5.2 3 | author: Tencent Cloud, Inc. 4 | org: Tencent Cloud, Inc. 5 | description: WebSocket 组件,允许用户开发部署基于 Socket 的函数应用。 6 | keywords: tencent, serverless, websocket 7 | repo: https://github.com/serverless-components/tencent-websocket 8 | readme: https://github.com/serverless-components/tencent-websocket/tree/master/README.md 9 | license: MIT 10 | main: ./src 11 | -------------------------------------------------------------------------------- /src/_shims/sl_handler.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const http = require('http') 3 | const url = require('url') 4 | 5 | const EventMaps = { 6 | connect: 'connecting', 7 | disconnect: 'closing', 8 | message: 'data send', 9 | default: 'default' 10 | } 11 | 12 | const isJson = (body) => { 13 | try { 14 | JSON.parse(body) 15 | } catch (e) { 16 | return false 17 | } 18 | return true 19 | } 20 | 21 | async function request(event, data = '', id = '') { 22 | const { websocket } = event 23 | const { action, secConnectionID, secWebSocketProtocol, secWebSocketExtensions } = websocket 24 | const retmsg = { 25 | websocket: { 26 | action: action, 27 | secConnectionID: id ? id : secConnectionID, 28 | dataType: 'text', 29 | data: data 30 | } 31 | } 32 | if (secWebSocketProtocol) { 33 | retmsg.websocket.secWebSocketProtocol = secWebSocketProtocol 34 | } 35 | if (secWebSocketExtensions) { 36 | retmsg.websocket.secWebSocketExtensions = secWebSocketExtensions 37 | } 38 | 39 | const postData = JSON.stringify(retmsg) 40 | await new Promise((resolve) => { 41 | const urlObj = url.parse(process.env.wsBackUrl) 42 | const req = http.request( 43 | { 44 | method: 'POST', 45 | host: urlObj.host, 46 | path: urlObj.path, 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | 'Content-Length': Buffer.byteLength(postData) 50 | } 51 | }, 52 | (res) => { 53 | // console.log(`STATUS: ${res.statusCode}`); 54 | // console.log(`HEADERS: ${JSON.stringify(res.headers)}`); 55 | res.setEncoding('utf8') 56 | res.on('data', (chunk) => { 57 | /* eslint-disable no-console */ 58 | console.log(`BODY: ${chunk}`) 59 | }) 60 | // res.on("end", () => { 61 | // console.log("No more data in response."); 62 | // }); 63 | resolve() 64 | } 65 | ) 66 | 67 | req.on('error', (e) => { 68 | /* eslint-disable no-console */ 69 | console.error(`problem with request: ${e.message}`) 70 | }) 71 | 72 | // write data to request body 73 | req.write(postData) 74 | req.end() 75 | }) 76 | } 77 | 78 | module.exports.handler = (event, context) => { 79 | context.callbackWaitsForEmptyEventLoop = false 80 | 81 | const { websocket } = event 82 | const { secConnectionID, action, data } = websocket 83 | 84 | return new Promise((resolve) => { 85 | const exit = (returnValue = '') => { 86 | resolve({ 87 | errNo: 0, 88 | errMsg: 'ok', 89 | data: returnValue, 90 | websocket: { 91 | action: action, 92 | secConnectionID: secConnectionID 93 | } 94 | }) 95 | } 96 | 97 | const socket = { 98 | id: secConnectionID, 99 | event: event, 100 | send: async (sendData, id) => { 101 | return request(event, sendData, id) 102 | } 103 | } 104 | // we can make an exception for this single case 105 | // for the sake of UX 106 | global.on = async (route, fn) => { 107 | route = EventMaps[route] 108 | if (!route) { 109 | throw new Error(`Unknow event: ${route}`) 110 | } 111 | if (route === action) { 112 | if (action === 'data send') { 113 | if (isJson(data)) { 114 | const parsedData = JSON.parse(data) 115 | const response = await fn(parsedData, socket) 116 | exit(response) 117 | } else { 118 | const response = await fn(data, socket) 119 | exit(response) 120 | } 121 | } else { 122 | const response = await fn(null, socket) 123 | exit(response) 124 | } 125 | } 126 | } 127 | 128 | const entryFile = path.join(__dirname, process.env.SLS_ENTRY_FILE) 129 | delete require.cache[require.resolve(entryFile)] 130 | require(entryFile) 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const CONFIGS = () => { 2 | return { 3 | injectSlsSdk: true, 4 | compName: 'websocket', 5 | compFullname: 'Websocket', 6 | defaultEntryFile: 'sls.js', 7 | region: 'ap-guangzhou', 8 | runtime: 'Nodejs10.15', 9 | description: 'Created by Serverless Component', 10 | handler: 'sl_handler.handler', 11 | timeout: 10, 12 | memorySize: 128, 13 | namespace: 'default', 14 | cos: { 15 | lifecycle: [ 16 | { 17 | status: 'Enabled', 18 | id: 'deleteObject', 19 | filter: '', 20 | expiration: { days: '10' }, 21 | abortIncompleteMultipartUpload: { daysAfterInitiation: '10' } 22 | } 23 | ] 24 | } 25 | } 26 | } 27 | 28 | module.exports = CONFIGS 29 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@slsplus/migrate": "^0.0.1-beta-4", 4 | "download": "^8.0.0", 5 | "tencent-component-toolkit": "^2.5.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/serverless.js: -------------------------------------------------------------------------------- 1 | const { Component } = require('@serverless/core') 2 | const { Scf, Apigw } = require('tencent-component-toolkit') 3 | const { ApiTypeError } = require('tencent-component-toolkit/lib/utils/error') 4 | const { migrateFramework } = require('@slsplus/migrate') 5 | const { uploadCodeToCos, getDefaultProtocol, initializeInputs, deepClone } = require('./utils') 6 | const initConfigs = require('./config') 7 | 8 | class ServerlessComponent extends Component { 9 | getCredentials() { 10 | const { tmpSecrets } = this.credentials.tencent 11 | 12 | if (!tmpSecrets || !tmpSecrets.TmpSecretId) { 13 | throw new ApiTypeError( 14 | 'CREDENTIAL', 15 | 'Cannot get secretId/Key, your account could be sub-account and does not have the access to use SLS_QcsRole, please make sure the role exists first, then visit https://cloud.tencent.com/document/product/1154/43006, follow the instructions to bind the role to your account.' 16 | ) 17 | } 18 | 19 | return { 20 | SecretId: tmpSecrets.TmpSecretId, 21 | SecretKey: tmpSecrets.TmpSecretKey, 22 | Token: tmpSecrets.Token 23 | } 24 | } 25 | 26 | getAppId() { 27 | return this.credentials.tencent.tmpSecrets.appId 28 | } 29 | 30 | initialize(framework = 'websocket') { 31 | const CONFIGS = initConfigs(framework) 32 | this.CONFIGS = CONFIGS 33 | this.framework = framework 34 | this.__TmpCredentials = this.getCredentials() 35 | } 36 | 37 | async deployFaas(credentials, inputs) { 38 | const appId = this.getAppId() 39 | const { region } = inputs 40 | const { state } = this 41 | const instance = this 42 | const funcDeployer = async () => { 43 | const code = await uploadCodeToCos(instance, appId, credentials, inputs, region) 44 | const scf = new Scf(credentials, region) 45 | const tempInputs = { 46 | ...inputs, 47 | code 48 | } 49 | const scfOutput = await scf.deploy(deepClone(tempInputs)) 50 | const outputs = { 51 | name: scfOutput.FunctionName, 52 | runtime: scfOutput.Runtime, 53 | namespace: scfOutput.Namespace 54 | } 55 | 56 | // default version is $LATEST 57 | outputs.lastVersion = scfOutput.LastVersion 58 | ? scfOutput.LastVersion 59 | : (state.faas && state.faas.lastVersion) || '$LATEST' 60 | 61 | // default traffic is 1.0, it can also be 0, so we should compare to undefined 62 | outputs.traffic = 63 | scfOutput.Traffic !== undefined 64 | ? scfOutput.Traffic 65 | : (state.faas && state.faas.traffic) !== undefined 66 | ? state.faas.traffic 67 | : 1 68 | 69 | if (outputs.traffic !== 1 && scfOutput.ConfigTrafficVersion) { 70 | outputs.configTrafficVersion = scfOutput.ConfigTrafficVersion 71 | } 72 | 73 | return outputs 74 | } 75 | 76 | const faasOutputs = await funcDeployer(region) 77 | 78 | this.state.faas = faasOutputs 79 | await this.save() 80 | 81 | return faasOutputs 82 | } 83 | 84 | async deployApigw(credentials, inputs) { 85 | if (inputs.isDisabled) { 86 | return {} 87 | } 88 | 89 | const { region } = inputs 90 | const { state } = this 91 | 92 | const apigwDeployer = async () => { 93 | const apigw = new Apigw(credentials, region) 94 | 95 | const oldState = state.apigw || {} 96 | const apigwInputs = { 97 | ...inputs, 98 | oldState: { 99 | apis: oldState.apis || [], 100 | customDomains: oldState.customDomains || [] 101 | } 102 | } 103 | // different region deployment has different service id 104 | apigwInputs.serviceId = inputs.id || (state.apigw && state.apigw.id) 105 | const apigwOutput = await apigw.deploy(deepClone(apigwInputs)) 106 | apigwOutput.apiList = apigwOutput.apiList.map((item) => { 107 | item.created = true 108 | return item 109 | }) 110 | const outputs = { 111 | url: `${getDefaultProtocol(apigwInputs.protocols) === 'https' ? 'wss' : 'ws'}://${ 112 | apigwOutput.subDomain 113 | }/${apigwOutput.environment}${apigwInputs.endpoints[0].path}`, 114 | id: apigwOutput.serviceId, 115 | domain: apigwOutput.subDomain, 116 | environment: apigwOutput.environment, 117 | wsBackUrl: apigwOutput.apiList[0].internalDomain, 118 | apis: apigwOutput.apiList 119 | } 120 | 121 | if (apigwOutput.customDomains) { 122 | outputs.customDomains = apigwOutput.customDomains 123 | } 124 | return outputs 125 | } 126 | 127 | const apigwOutputs = await apigwDeployer() 128 | 129 | this.state.apigw = apigwOutputs 130 | await this.save() 131 | 132 | return apigwOutputs 133 | } 134 | 135 | async updateFaas(credentials, inputs, wsBackUrl) { 136 | // after websocket api create, we should add wsBackUrl environment for cloud function 137 | console.log(`Start add wsBackUrl environment variable for function ${inputs.name}`) 138 | inputs.environment = inputs.environment || {} 139 | inputs.environment.variables.wsBackUrl = wsBackUrl 140 | 141 | const scf = new Scf(credentials, inputs.region) 142 | await scf.scf.updateConfigure(inputs) 143 | 144 | console.log(`Add wsBackUrl environment variable for function ${inputs.name} successfully`) 145 | } 146 | 147 | async deploy(inputs) { 148 | inputs = migrateFramework(inputs) 149 | 150 | this.initialize() 151 | const { __TmpCredentials, CONFIGS } = this 152 | 153 | console.log(`Deploying ${this.framework} Application`) 154 | 155 | const { region, faasConfig, apigwConfig } = await initializeInputs(this, inputs) 156 | 157 | const outputs = { 158 | region 159 | } 160 | if (!faasConfig.code.src) { 161 | outputs.templateUrl = CONFIGS.templateUrl 162 | } 163 | 164 | const faasOutputs = await this.deployFaas(__TmpCredentials, faasConfig) 165 | let apigwOutputs 166 | // support apigw.isDisabled 167 | if (apigwConfig.isDisabled !== true) { 168 | apigwOutputs = await this.deployApigw(__TmpCredentials, apigwConfig) 169 | } else { 170 | this.state.apigw.isDisabled = true 171 | } 172 | 173 | const { wsBackUrl } = apigwOutputs 174 | 175 | await this.updateFaas(__TmpCredentials, faasConfig, wsBackUrl) 176 | 177 | outputs['faas'] = faasOutputs 178 | outputs['apigw'] = apigwOutputs 179 | 180 | // this config for online debug 181 | this.state.region = region 182 | this.state.namespace = faasConfig.namespace 183 | this.state.lambdaArn = faasConfig.name 184 | 185 | return outputs 186 | } 187 | 188 | async remove() { 189 | this.initialize() 190 | const { __TmpCredentials, framework } = this 191 | 192 | console.log(`Removing ${framework} App`) 193 | 194 | const { state } = this 195 | const { region } = state 196 | 197 | let { faas: faasState, apigw: apigwState } = state 198 | if (!faasState) { 199 | const curState = state[region] 200 | faasState = { 201 | name: curState.name, 202 | namespace: curState.namespace 203 | } 204 | } 205 | if (!apigwState) { 206 | const curState = state[region] 207 | apigwState = { 208 | id: curState.serviceId, 209 | environment: curState.environment, 210 | apis: curState.apiList, 211 | customDomains: curState.customDomains 212 | } 213 | } 214 | const scf = new Scf(__TmpCredentials, region) 215 | const apigw = new Apigw(__TmpCredentials, region) 216 | try { 217 | // if disable apigw, no need to remove 218 | const serviceId = apigwState.id || apigwState.serviceId 219 | if (apigwState.isDisabled !== true && serviceId) { 220 | apigwState.apis = apigwState.apis.map((item) => { 221 | item.created = true 222 | return item 223 | }) 224 | await apigw.remove({ 225 | created: true, 226 | serviceId: serviceId, 227 | environment: apigwState.environment, 228 | apiList: apigwState.apis || apigwState.apiList || [], 229 | customDomains: apigwState.customDomains 230 | }) 231 | } 232 | 233 | await scf.remove({ 234 | functionName: faasState.name, 235 | namespace: faasState.namespace 236 | }) 237 | } catch (e) { 238 | console.log(e) 239 | } 240 | 241 | this.state = {} 242 | 243 | return {} 244 | } 245 | } 246 | 247 | module.exports = ServerlessComponent 248 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const { Cos } = require('tencent-component-toolkit') 4 | const download = require('download') 5 | const { ApiTypeError } = require('tencent-component-toolkit/lib/utils/error') 6 | 7 | /* 8 | * Generates a random id 9 | */ 10 | const generateId = () => 11 | Math.random() 12 | .toString(36) 13 | .substring(6) 14 | 15 | const deepClone = (obj) => { 16 | return JSON.parse(JSON.stringify(obj)) 17 | } 18 | 19 | const capitalString = (str) => { 20 | if (str.length < 2) { 21 | return str.toUpperCase() 22 | } 23 | 24 | return `${str[0].toUpperCase()}${str.slice(1)}` 25 | } 26 | 27 | const getTimestamp = () => { 28 | return Math.floor(Date.now() / 1000) 29 | } 30 | 31 | const getDefaultProtocol = (protocols) => { 32 | return String(protocols).includes('https') ? 'https' : 'http' 33 | } 34 | 35 | const getDefaultFunctionName = (framework) => { 36 | return `${framework}-${generateId()}` 37 | } 38 | 39 | const getDefaultServiceName = () => { 40 | return 'serverless' 41 | } 42 | 43 | const getDefaultServiceDescription = () => { 44 | return 'Created by Serverless Component' 45 | } 46 | 47 | const getDefaultBucketName = (region) => { 48 | return `serverless-${region}-code` 49 | } 50 | 51 | const getDefaultObjectName = (inputs) => { 52 | return `${inputs.name}-${getTimestamp()}.zip` 53 | } 54 | 55 | const getDirFiles = (dirPath) => { 56 | const targetPath = path.resolve(dirPath) 57 | const files = fs.readdirSync(targetPath) 58 | const temp = {} 59 | files.forEach((file) => { 60 | temp[file] = path.join(targetPath, file) 61 | }) 62 | return temp 63 | } 64 | 65 | const getCodeZipPath = async (instance, inputs) => { 66 | const { CONFIGS, framework } = instance 67 | console.log(`Packaging ${framework} application`) 68 | 69 | // unzip source zip file 70 | let zipPath 71 | if (!inputs.code.src) { 72 | // add default template 73 | const downloadPath = `/tmp/${generateId()}` 74 | const filename = 'template' 75 | 76 | console.log(`Downloading default ${framework} application`) 77 | try { 78 | await download(CONFIGS.templateUrl, downloadPath, { 79 | filename: `${filename}.zip` 80 | }) 81 | } catch (e) { 82 | throw new ApiTypeError(`DOWNLOAD_TEMPLATE`, 'Download default template failed.') 83 | } 84 | zipPath = `${downloadPath}/${filename}.zip` 85 | } else { 86 | zipPath = inputs.code.src 87 | } 88 | 89 | return zipPath 90 | } 91 | 92 | // get files/dirs need to inject to project code 93 | const getInjection = () => { 94 | let injectFiles = {} 95 | const injectDirs = {} 96 | const shimPath = path.join(__dirname, '_shims') 97 | injectFiles = getDirFiles(shimPath) 98 | 99 | return { injectFiles, injectDirs } 100 | } 101 | 102 | /** 103 | * Upload code to COS 104 | * @param {Component} instance serverless component instance 105 | * @param {string} appId app id 106 | * @param {object} credentials credentials 107 | * @param {object} inputs component inputs parameters 108 | * @param {string} region region 109 | */ 110 | const uploadCodeToCos = async (instance, appId, credentials, inputs, region) => { 111 | const { CONFIGS, framework } = instance 112 | const bucketName = inputs.code.bucket || getDefaultBucketName(region) 113 | const objectName = inputs.code.object || getDefaultObjectName(inputs) 114 | const bucket = `${bucketName}-${appId}` 115 | 116 | const zipPath = await getCodeZipPath(instance, inputs) 117 | console.log(`Code zip path ${zipPath}`) 118 | 119 | // save the zip path to state for lambda to use it 120 | instance.state.zipPath = zipPath 121 | 122 | const cos = new Cos(credentials, region) 123 | 124 | if (!inputs.code.bucket) { 125 | // create default bucket 126 | await cos.deploy({ 127 | force: true, 128 | bucket: bucketName + '-' + appId, 129 | lifecycle: CONFIGS.cos.lifecycle 130 | }) 131 | } 132 | if (!inputs.code.object) { 133 | console.log(`Getting cos upload url for bucket ${bucketName}`) 134 | const uploadUrl = await cos.getObjectUrl({ 135 | bucket: bucket, 136 | object: objectName, 137 | method: 'PUT' 138 | }) 139 | 140 | // if shims and sls sdk entries had been injected to zipPath, no need to injected again 141 | console.log(`Uploading code to bucket ${bucketName}`) 142 | 143 | const { injectFiles, injectDirs } = getInjection(instance, framework) 144 | 145 | await instance.uploadSourceZipToCOS(zipPath, uploadUrl, injectFiles, injectDirs) 146 | console.log(`Upload ${objectName} to bucket ${bucketName} success`) 147 | } 148 | 149 | // save bucket state 150 | instance.state.bucket = bucketName 151 | instance.state.object = objectName 152 | 153 | return { 154 | bucket: bucketName, 155 | object: objectName 156 | } 157 | } 158 | 159 | // compatible code for old configs 160 | // transfer yaml config to sdk inputs 161 | const yamlToSdkInputs = ({ instance, faasConfig, apigwConfig }) => { 162 | // transfer faas config 163 | if (faasConfig.environments) { 164 | // this is new config array to object 165 | const environment = deepClone(faasConfig.environments) 166 | faasConfig.environment = { 167 | variables: { 168 | SERVERLESS: '1', 169 | SLS_ENTRY_FILE: instance.slsEntryFile 170 | } 171 | } 172 | environment.forEach((item) => { 173 | faasConfig.environment.variables[item.key] = item.value 174 | }) 175 | } else { 176 | faasConfig.environment = { 177 | variables: { 178 | SERVERLESS: '1', 179 | SLS_ENTRY_FILE: instance.slsEntryFile 180 | } 181 | } 182 | } 183 | 184 | if (faasConfig.vpc) { 185 | faasConfig.vpcConfig = faasConfig.vpc 186 | } 187 | 188 | if (faasConfig.tags) { 189 | const tags = deepClone(faasConfig.tags) 190 | faasConfig.tags = {} 191 | tags.forEach((item) => { 192 | faasConfig.tags[item.key] = item.value 193 | }) 194 | } 195 | 196 | // transfer apigw config 197 | apigwConfig.serviceId = apigwConfig.id 198 | apigwConfig.serviceName = apigwConfig.name || getDefaultServiceName(instance) 199 | apigwConfig.serviceDesc = apigwConfig.description || getDefaultServiceDescription(instance) 200 | 201 | if (apigwConfig.customDomains && apigwConfig.customDomains.length > 0) { 202 | apigwConfig.customDomains = apigwConfig.customDomains.map((item) => { 203 | return { 204 | domain: item.domain, 205 | certificateId: item.certId, 206 | isDefaultMapping: !item.customMap, 207 | pathMappingSet: item.pathMap, 208 | protocols: item.protocols 209 | } 210 | }) 211 | } 212 | 213 | return { faasConfig, apigwConfig } 214 | } 215 | 216 | const initializeInputs = async (instance, inputs = {}) => { 217 | const { CONFIGS, state } = instance 218 | const region = inputs.region || CONFIGS.region 219 | 220 | // chenck state function name 221 | const stateFaasName = state.faas && state.faas.name 222 | const stateApigwId = state.apigw && (state.apigw.id || state.apigw.serviceId) 223 | 224 | const tempFaasConfig = inputs.faas || {} 225 | const faasConfig = Object.assign(tempFaasConfig, { 226 | region: region, 227 | code: { 228 | src: inputs.src.src, 229 | bucket: inputs.srcOriginal && inputs.srcOriginal.bucket, 230 | object: inputs.srcOriginal && inputs.srcOriginal.object 231 | }, 232 | name: tempFaasConfig.name || stateFaasName || getDefaultFunctionName(), 233 | role: tempFaasConfig.role || '', 234 | handler: tempFaasConfig.handler || CONFIGS.handler, 235 | runtime: tempFaasConfig.runtime || CONFIGS.runtime, 236 | namespace: tempFaasConfig.namespace || CONFIGS.namespace, 237 | description: tempFaasConfig.description || CONFIGS.description, 238 | layers: tempFaasConfig.layers || [], 239 | cfs: tempFaasConfig.cfs || [], 240 | timeout: tempFaasConfig.timeout || CONFIGS.timeout, 241 | memorySize: tempFaasConfig.memorySize || CONFIGS.memorySize 242 | }) 243 | 244 | const slsEntryFile = inputs.entryFile || CONFIGS.defaultEntryFile 245 | instance.slsEntryFile = slsEntryFile 246 | 247 | const tempApigwConfig = inputs.apigw || {} 248 | const apigwConfig = Object.assign(tempApigwConfig, { 249 | region, 250 | id: tempApigwConfig.id || stateApigwId, 251 | isDisabled: tempApigwConfig.isDisabled === true, 252 | protocols: tempApigwConfig.protocols || ['http'], 253 | environment: tempApigwConfig.environment || 'release', 254 | endpoints: [ 255 | { 256 | path: '/', 257 | apiName: 'index', 258 | method: 'GET', 259 | enableCORS: tempApigwConfig.cors, 260 | serviceTimeout: tempApigwConfig.timeout, 261 | protocol: 'WEBSOCKET', 262 | function: { 263 | isIntegratedResponse: false, 264 | functionQualifier: tempApigwConfig.qualifier || '$DEFAULT', 265 | functionName: faasConfig.name, 266 | functionNamespace: faasConfig.namespace, 267 | transportFunctionName: faasConfig.name, 268 | registerFunctionName: faasConfig.name, 269 | cleanupFunctionName: faasConfig.name 270 | } 271 | } 272 | ] 273 | }) 274 | 275 | return { 276 | region, 277 | ...yamlToSdkInputs({ instance, faasConfig, apigwConfig }) 278 | } 279 | } 280 | 281 | module.exports = { 282 | deepClone, 283 | generateId, 284 | uploadCodeToCos, 285 | capitalString, 286 | getDefaultProtocol, 287 | initializeInputs 288 | } 289 | --------------------------------------------------------------------------------