├── .npmignore ├── .npmrc ├── src ├── framework.yml ├── handler.js ├── serverless.js ├── _shims │ ├── package.json │ ├── sls.js │ └── handler.js ├── package.json ├── config.js ├── utils.js ├── formatter.js └── index.js ├── .eslintignore ├── example ├── public │ ├── favicon.ico │ └── vercel.svg ├── .env.example ├── pages │ ├── _app.js │ ├── api │ │ └── hello.js │ └── index.js ├── next.config.js ├── package.json ├── styles │ ├── globals.css │ └── Home.module.css ├── serverless.yml ├── .gitignore ├── sls.express.js └── sls.koa.js ├── .prettierignore ├── prettier.config.js ├── commitlint.config.js ├── .editorconfig ├── serverless.component.yml ├── jest.config.js ├── .gitignore ├── __tests__ ├── lib │ └── utils.js └── index.test.js ├── docs ├── upload.md └── configure.md ├── LICENSE ├── release.config.js ├── .github └── workflows │ ├── validate.yml │ ├── release.yml │ └── test.yml ├── .eslintrc.js ├── package.json ├── README.md └── CHANGELOG.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /src/framework.yml: -------------------------------------------------------------------------------- 1 | name: nextjs 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | example 5 | *.test.js 6 | src/_src 7 | -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | const { handler } = require('@serverless/core') 2 | module.exports.handler = handler 3 | -------------------------------------------------------------------------------- /src/serverless.js: -------------------------------------------------------------------------------- 1 | const index_1 = require('./index') 2 | module.exports = index_1.ServerlessComponent 3 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/tencent-nextjs/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /example/.env.example: -------------------------------------------------------------------------------- 1 | TENCENT_SECRET_ID=123 2 | TENCENT_SECRET_KEY=123 3 | 4 | STATIC_URL=https://nextjs-demo-123456789.cos.ap-guangzhou.myqcloud.com 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | CHANGELOG.md 5 | *.test.js 6 | tests/src/.next 7 | tests/src/node_modules 8 | 9 | .next 10 | -------------------------------------------------------------------------------- /example/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /src/_shims/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "express": "^4.17.1", 4 | "tencent-component-monitor": "^1.1.0", 5 | "tencent-serverless-http": "^1.3.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "adm-zip": "^0.4.16", 4 | "download": "^8.0.0", 5 | "fs-extra": "^9.1.0", 6 | "js-yaml": "^4.0.0", 7 | "tencent-component-toolkit": "2.23.3", 8 | "type": "^2.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/next.config.js: -------------------------------------------------------------------------------- 1 | const isProd = process.env.NODE_ENV === 'production' 2 | 3 | module.exports = { 4 | env: { 5 | STATIC_URL: isProd ? process.env.STATIC_URL : '' 6 | }, 7 | assetPrefix: isProd ? process.env.STATIC_URL : '', 8 | reactStrictMode: true, 9 | eslint: { 10 | ignoreDuringBuilds: true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "^11.0.1", 12 | "react": "^17.0.2", 13 | "react-dom": "^17.0.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, 6 | Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /example/serverless.yml: -------------------------------------------------------------------------------- 1 | stage: dev 2 | component: nextjs 3 | name: nextjsDemo 4 | 5 | inputs: 6 | src: 7 | dist: ./ 8 | hook: npm run build 9 | exclude: 10 | - .env 11 | region: ap-guangzhou 12 | runtime: Nodejs10.15 13 | apigatewayConf: 14 | protocols: 15 | - http 16 | - https 17 | environment: release 18 | staticConf: 19 | cosConf: 20 | replace: true 21 | bucket: nextjs-demo 22 | -------------------------------------------------------------------------------- /serverless.component.yml: -------------------------------------------------------------------------------- 1 | name: nextjs 2 | version: 1.0.14 3 | author: 'Tencent Cloud, Inc.' 4 | org: 'Tencent Cloud, Inc.' 5 | description: Deploy a serverless Next.js application onto Tencent SCF and API Gateway. 6 | keywords: 'tencent, serverless, next.js' 7 | repo: 'https://github.com/serverless-components/tencent-nextjs' 8 | readme: 'https://github.com/serverless-components/tencent-nextjs/tree/master/README.md' 9 | license: MIT 10 | main: ./src 11 | webDeployable: true 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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.example 21 | env.js 22 | package-lock.json 23 | test 24 | yarn.lock 25 | test/src/node_modules 26 | test/src/.next 27 | test/src/package-lock.json 28 | -------------------------------------------------------------------------------- /__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) => { 16 | const sdk = new ServerlessSDK({ 17 | context: { 18 | orgName 19 | } 20 | }) 21 | return sdk 22 | } 23 | 24 | module.exports = { generateId, getServerlessSdk } 25 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # debug 38 | npm-debug.log* 39 | yarn-debug.log* 40 | yarn-error.log* 41 | 42 | .serverless 43 | .next 44 | -------------------------------------------------------------------------------- /src/_shims/sls.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const next = require('next') 3 | 4 | async function createServer() { 5 | const app = next({ dev: false }) 6 | const handle = app.getRequestHandler() 7 | 8 | // not report route for custom monitor 9 | const noReportRoutes = ['/_next', '/favicon.ico'] 10 | 11 | await app.prepare() 12 | const server = express() 13 | 14 | server.all('*', (req, res) => { 15 | noReportRoutes.forEach((route) => { 16 | if (req.path.indexOf(route) === 0) { 17 | req.__SLS_NO_REPORT__ = true 18 | } 19 | }) 20 | return handle(req, res) 21 | }) 22 | 23 | // define binary type for response 24 | // if includes, will return base64 encoded, very useful for images 25 | server.binaryTypes = ['*/*'] 26 | 27 | // 返回 server 28 | return server 29 | } 30 | 31 | module.exports = createServer 32 | -------------------------------------------------------------------------------- /docs/upload.md: -------------------------------------------------------------------------------- 1 | ## 文件上传说明 2 | 3 | 项目中如果涉及到文件上传,需要依赖 API 网关提供的 [Base64 编码能力](https://cloud.tencent.com/document/product/628/51799),使用时只需要 `serverless.yml` 中配置 `isBase64Encoded` 为 `true`,如下: 4 | 5 | ```yaml 6 | app: appDemo 7 | stage: dev 8 | component: nextjs 9 | name: nextjsDemo 10 | 11 | inputs: 12 | # 省略... 13 | apigatewayConf: 14 | isBase64Encoded: true 15 | # 省略... 16 | # 省略... 17 | ``` 18 | 19 | 当前 API 网关支持上传最大文件大小为 `2M`,如果文件过大,请修改为前端直传对象存储方案。 20 | 21 | ## Base64 示例 22 | 23 | 此 Github 项目的 `example` 目录下存在两个模板文件: 24 | 25 | - [sls.express.js](../example/sls.express.js) 26 | - [sls.koa.js](../example/sls.koa.js) 27 | 28 | 开发者可根据个人项目需要参考修改,使用时需要复制对应文件名为 `sls.js`。 29 | 30 | 两个模板文件中均实现了文件上传接口 `POST /upload`。使用 Koa 的项目,如果要支持文件上传,需要安装 `@koajs/multer` 和 `multer` 包。使用 Express 的项目,如果要支持文件上传,需要安装 `multer` 包。 31 | 32 | 同时需要在 `serverless.yml` 的 `apigatewayConf` 中配置 `isBase64Encoded` 为 `true`。 33 | -------------------------------------------------------------------------------- /example/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import styles from '../styles/Home.module.css' 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 | Create Next App 9 | 10 | 11 | 12 |
13 |

Welcome to Next.js!

14 | 15 |

16 | Get started by editing pages/index.js 17 |

18 |

19 | The SSR app is hosted on  20 | 21 | Serverless SSR 22 | 23 |

24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /example/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/sls.express.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer') 2 | const express = require('express') 3 | const next = require('next') 4 | 5 | const isServerless = process.env.SERVERLESS 6 | 7 | async function createServer() { 8 | const upload = multer({ dest: isServerless ? '/tmp/upload' : './upload' }) 9 | 10 | const server = express() 11 | const app = next({ dev: false }) 12 | const handle = app.getRequestHandler() 13 | 14 | server.post('/upload', upload.single('file'), (req, res) => { 15 | res.send({ 16 | success: true, 17 | data: req.file 18 | }) 19 | }) 20 | 21 | server.all('*', (req, res, next) => { 22 | return handle(req, res) 23 | }) 24 | 25 | // define binary type for response 26 | // if includes, will return base64 encoded, very useful for images 27 | server.binaryTypes = ['*/*'] 28 | 29 | return server 30 | } 31 | 32 | module.exports = createServer 33 | 34 | if (isServerless) { 35 | module.exports = createServer 36 | } else { 37 | createServer().then((server) => { 38 | server.listen(3000, () => { 39 | console.log(`Server start on http://localhost:3000`) 40 | }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/sls.koa.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const Router = require('@koa/router') 3 | const multer = require('@koa/multer') 4 | const next = require('next') 5 | 6 | const isServerless = process.env.SERVERLESS 7 | 8 | async function createServer() { 9 | const server = new Koa() 10 | const router = new Router() 11 | const upload = multer({ dest: isServerless ? '/tmp/upload' : './upload' }) 12 | const app = next({ dev: false }) 13 | const handle = app.getRequestHandler() 14 | 15 | router.post('/upload', upload.single('file'), (ctx) => { 16 | ctx.body = { 17 | success: true, 18 | data: ctx.file 19 | } 20 | }) 21 | 22 | server.use(router.routes()).use(router.allowedMethods()) 23 | 24 | server.use((ctx) => { 25 | ctx.status = 200 26 | ctx.respond = false 27 | ctx.req.ctx = ctx 28 | 29 | return handle(ctx.req, ctx.res) 30 | }) 31 | 32 | // define binary type for response 33 | // if includes, will return base64 encoded, very useful for images 34 | server.binaryTypes = ['*/*'] 35 | 36 | return server 37 | } 38 | 39 | if (process.env.SERVERLESS) { 40 | module.exports = createServer 41 | } else { 42 | createServer().then((server) => { 43 | server.listen(3000, () => { 44 | console.log(`Server start on http://localhost:3000`) 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} 47 | TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} 48 | -------------------------------------------------------------------------------- /src/_shims/handler.js: -------------------------------------------------------------------------------- 1 | try { 2 | require('tencent-component-monitor') 3 | } catch (e) { 4 | console.log(e) 5 | } 6 | const fs = require('fs') 7 | const path = require('path') 8 | const { createServer, proxy } = require('tencent-serverless-http') 9 | 10 | let server 11 | let app 12 | 13 | module.exports.handler = async (event, context) => { 14 | if (!app) { 15 | const userSls = path.join(__dirname, '..', process.env.SLS_ENTRY_FILE) 16 | if (fs.existsSync(userSls)) { 17 | // eslint-disable-next-line 18 | console.log(`Using user custom entry file ${process.env.SLS_ENTRY_FILE}`) 19 | app = await require(userSls)(true) 20 | } else { 21 | app = await require('./sls')(false) 22 | } 23 | 24 | // provide sls intialize hooks 25 | if (app.slsInitialize && typeof app.slsInitialize === 'function') { 26 | await app.slsInitialize() 27 | } 28 | } 29 | 30 | // attach event and context to request 31 | try { 32 | app.request.__SLS_EVENT__ = event 33 | app.request.__SLS_CONTEXT__ = context 34 | } catch (e) { 35 | // no op 36 | } 37 | 38 | // do not cache server, so we can pass latest event to server 39 | server = createServer( 40 | app.callback && typeof app.callback === 'function' ? app.callback() : app, 41 | null, 42 | app.binaryTypes || [] 43 | ) 44 | 45 | context.callbackWaitsForEmptyEventLoop = app.callbackWaitsForEmptyEventLoop === true 46 | 47 | const { promise } = await proxy(server, event, context, 'PROMISE') 48 | return promise 49 | } 50 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const { generateId, getServerlessSdk } = require('./lib/utils') 2 | const axios = require('axios') 3 | 4 | const instanceYaml = { 5 | org: 'orgDemo', 6 | app: 'appDemo', 7 | component: 'nextjs@dev', 8 | name: `nextjs-integration-tests-${generateId()}`, 9 | stage: 'dev', 10 | inputs: { 11 | region: 'ap-guangzhou', 12 | runtime: 'Nodejs10.15', 13 | apigatewayConf: { environment: 'test' } 14 | } 15 | } 16 | 17 | const credentials = { 18 | tencent: { 19 | SecretId: process.env.TENCENT_SECRET_ID, 20 | SecretKey: process.env.TENCENT_SECRET_KEY, 21 | } 22 | } 23 | 24 | const sdk = getServerlessSdk(instanceYaml.org) 25 | 26 | it('should successfully deploy nextjs app', async () => { 27 | const instance = await sdk.deploy(instanceYaml, credentials) 28 | 29 | expect(instance).toBeDefined() 30 | expect(instance.instanceName).toEqual(instanceYaml.name) 31 | expect(instance.outputs).toBeDefined() 32 | // get src from template by default 33 | expect(instance.outputs.templateUrl).toBeDefined() 34 | expect(instance.outputs.region).toEqual(instanceYaml.inputs.region) 35 | expect(instance.outputs.scf).toBeDefined() 36 | expect(instance.outputs.scf.runtime).toEqual(instanceYaml.inputs.runtime) 37 | expect(instance.outputs.apigw).toBeDefined() 38 | expect(instance.outputs.apigw.environment).toEqual(instanceYaml.inputs.apigatewayConf.environment) 39 | 40 | const response = await axios.get(instance.outputs.apigw.url) 41 | expect(response.data.includes('Next.js!')).toBeTruthy() 42 | }) 43 | 44 | it('should successfully remove nextjs app', async () => { 45 | await sdk.remove(instanceYaml, credentials) 46 | result = await sdk.getInstance(instanceYaml.org, instanceYaml.stage, instanceYaml.app, instanceYaml.name) 47 | 48 | expect(result.instance.instanceStatus).toEqual('inactive') 49 | }) 50 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /example/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a, 37 | .description a { 38 | color: #0070f3; 39 | text-decoration: none; 40 | } 41 | 42 | .title a:hover, 43 | .title a:focus, 44 | .title a:active { 45 | text-decoration: underline; 46 | } 47 | 48 | .title { 49 | margin: 0; 50 | line-height: 1.15; 51 | font-size: 4rem; 52 | } 53 | 54 | .title, 55 | .description { 56 | text-align: center; 57 | } 58 | 59 | .description { 60 | line-height: 1.5; 61 | font-size: 1.5rem; 62 | } 63 | 64 | .code { 65 | background: #fafafa; 66 | border-radius: 5px; 67 | padding: 0.75rem; 68 | font-size: 1.1rem; 69 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 70 | Bitstream Vera Sans Mono, Courier New, monospace; 71 | } 72 | 73 | .grid { 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | flex-wrap: wrap; 78 | max-width: 800px; 79 | margin-top: 3rem; 80 | } 81 | 82 | .card { 83 | margin: 1rem; 84 | padding: 1.5rem; 85 | text-align: left; 86 | color: inherit; 87 | text-decoration: none; 88 | border: 1px solid #eaeaea; 89 | border-radius: 10px; 90 | transition: color 0.15s ease, border-color 0.15s ease; 91 | width: 45%; 92 | } 93 | 94 | .card:hover, 95 | .card:focus, 96 | .card:active { 97 | color: #0070f3; 98 | border-color: #0070f3; 99 | } 100 | 101 | .card h2 { 102 | margin: 0 0 1rem 0; 103 | font-size: 1.5rem; 104 | } 105 | 106 | .card p { 107 | margin: 0; 108 | font-size: 1.25rem; 109 | line-height: 1.5; 110 | } 111 | 112 | .logo { 113 | height: 1em; 114 | margin-left: 0.5rem; 115 | } 116 | 117 | @media (max-width: 600px) { 118 | .grid { 119 | width: 100%; 120 | flex-direction: column; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless/nextjs", 3 | "main": "src/serverless.js", 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "description": "Tencent Cloud Next.js 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/npm": "^7.0.4", 46 | "@semantic-release/release-notes-generator": "^9.0.1", 47 | "@serverless/platform-client-china": "^1.0.19", 48 | "@ygkit/secure": "0.0.3", 49 | "axios": "^0.19.2", 50 | "babel-eslint": "^10.1.0", 51 | "dotenv": "^8.2.0", 52 | "eslint": "^6.8.0", 53 | "eslint-config-prettier": "^6.10.0", 54 | "eslint-plugin-import": "^2.20.1", 55 | "eslint-plugin-prettier": "^3.1.2", 56 | "husky": "^4.2.5", 57 | "jest": "^25.0.1", 58 | "lint-staged": "^10.0.8", 59 | "prettier": "^1.19.1", 60 | "semantic-release": "^17.0.4" 61 | }, 62 | "directories": { 63 | "doc": "docs", 64 | "example": "example", 65 | "test": "tests" 66 | }, 67 | "repository": { 68 | "type": "git", 69 | "url": "git+https://github.com/serverless-components/tencent-nextjs.git" 70 | }, 71 | "keywords": [ 72 | "serverless-nextjs", 73 | "nextjs", 74 | "serverless", 75 | "serverless-framework", 76 | "serverless-components", 77 | "tencent-cloud" 78 | ], 79 | "bugs": { 80 | "url": "https://github.com/serverless-components/tencent-nextjs/issues" 81 | }, 82 | "homepage": "https://github.com/serverless-components/tencent-nextjs#readme" 83 | } 84 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { value: true }) 2 | exports.getConfig = void 0 3 | const fs = require('fs') 4 | const path = require('path') 5 | const YAML = require('js-yaml') 6 | const TEMPLATE_BASE_URL = 'https://serverless-templates-1300862921.cos.ap-beijing.myqcloud.com' 7 | const frameworks = { 8 | express: { 9 | injectSlsSdk: true, 10 | runtime: 'Nodejs10.15', 11 | defaultEntryFile: 'sls.js', 12 | defaultStatics: [{ src: 'public', targetDir: '/' }] 13 | }, 14 | koa: { 15 | injectSlsSdk: true, 16 | runtime: 'Nodejs10.15', 17 | defaultEntryFile: 'sls.js', 18 | defaultStatics: [{ src: 'public', targetDir: '/' }] 19 | }, 20 | egg: { 21 | injectSlsSdk: true, 22 | runtime: 'Nodejs10.15', 23 | defaultEntryFile: 'sls.js', 24 | defaultStatics: [{ src: 'public', targetDir: '/' }], 25 | defaultEnvs: [ 26 | { 27 | key: 'SERVERLESS', 28 | value: '1' 29 | }, 30 | { 31 | key: 'EGG_APP_CONFIG', 32 | value: '{"rundir":"/tmp","logger":{"dir":"/tmp"}}' 33 | } 34 | ] 35 | }, 36 | nestjs: { 37 | injectSlsSdk: true, 38 | runtime: 'Nodejs10.15', 39 | defaultEntryFile: 'sls.js', 40 | defaultStatics: [{ src: 'public', targetDir: '/' }] 41 | }, 42 | nextjs: { 43 | injectSlsSdk: true, 44 | runtime: 'Nodejs10.15', 45 | defaultEntryFile: 'sls.js', 46 | defaultStatics: [ 47 | { src: '.next/static', targetDir: '/_next/static' }, 48 | { src: 'public', targetDir: '/' } 49 | ] 50 | }, 51 | nuxtjs: { 52 | injectSlsSdk: true, 53 | runtime: 'Nodejs10.15', 54 | defaultEntryFile: 'sls.js', 55 | defaultStatics: [ 56 | { src: '.nuxt/dist/client', targetDir: '/' }, 57 | { src: 'static', targetDir: '/' } 58 | ] 59 | }, 60 | laravel: { 61 | injectSlsSdk: false, 62 | runtime: 'Php7', 63 | defaultEnvs: [ 64 | { 65 | key: 'SERVERLESS', 66 | value: '1' 67 | }, 68 | { 69 | key: 'VIEW_COMPILED_PATH', 70 | value: '/tmp/storage/framework/views' 71 | }, 72 | { 73 | key: 'SESSION_DRIVER', 74 | value: 'array' 75 | }, 76 | { 77 | key: 'LOG_CHANNEL', 78 | value: 'stderr' 79 | }, 80 | { 81 | key: 'APP_STORAGE', 82 | value: '/tmp/storage' 83 | } 84 | ] 85 | }, 86 | thinkphp: { 87 | injectSlsSdk: false, 88 | runtime: 'Php7' 89 | }, 90 | flask: { 91 | injectSlsSdk: false, 92 | runtime: 'Python3.6' 93 | }, 94 | django: { 95 | injectSlsSdk: false, 96 | runtime: 'Python3.6' 97 | } 98 | } 99 | const CONFIGS = { 100 | // support metrics frameworks 101 | pythonFrameworks: ['flask', 'django'], 102 | supportMetrics: ['express', 'next', 'nuxt'], 103 | region: 'ap-guangzhou', 104 | description: 'Created by Serverless Component', 105 | handler: 'sl_handler.handler', 106 | timeout: 10, 107 | memorySize: 128, 108 | namespace: 'default', 109 | defaultEnvs: [ 110 | { 111 | key: 'SERVERLESS', 112 | value: '1' 113 | } 114 | ], 115 | cos: { 116 | lifecycle: [ 117 | { 118 | status: 'Enabled', 119 | id: 'deleteObject', 120 | expiration: { days: '10' }, 121 | abortIncompleteMultipartUpload: { daysAfterInitiation: '10' } 122 | } 123 | ] 124 | }, 125 | cdn: { 126 | forceRedirect: { 127 | switch: 'on', 128 | redirectType: 'https', 129 | redirectStatusCode: 301 130 | }, 131 | https: { 132 | switch: 'on', 133 | http2: 'on' 134 | } 135 | }, 136 | defaultCdnConfig: { 137 | forceRedirect: { 138 | switch: 'on', 139 | redirectType: 'https', 140 | redirectStatusCode: 301 141 | }, 142 | https: { 143 | switch: 'on', 144 | http2: 'on' 145 | } 146 | }, 147 | acl: { 148 | permissions: 'public-read', 149 | grantRead: '', 150 | grantWrite: '', 151 | grantFullControl: '' 152 | }, 153 | getPolicy(region, bucket, appid) { 154 | return { 155 | Statement: [ 156 | { 157 | Principal: { qcs: ['qcs::cam::anyone:anyone'] }, 158 | Effect: 'Allow', 159 | Action: [ 160 | 'name/cos:HeadBucket', 161 | 'name/cos:ListMultipartUploads', 162 | 'name/cos:ListParts', 163 | 'name/cos:GetObject', 164 | 'name/cos:HeadObject', 165 | 'name/cos:OptionsObject' 166 | ], 167 | Resource: [`qcs::cos:${region}:uid/${appid}:${bucket}/*`] 168 | } 169 | ], 170 | version: '2.0' 171 | } 172 | } 173 | } 174 | const getConfig = () => { 175 | const { name: framework } = YAML.load( 176 | // framework.yml 会在组件部署流程中动态生成 177 | fs.readFileSync(path.join(__dirname, 'framework.yml'), 'utf-8') 178 | ) 179 | const templateUrl = `${TEMPLATE_BASE_URL}/${framework}-demo.zip` 180 | const frameworkConfigs = frameworks[framework] 181 | return Object.assign(Object.assign({ framework, templateUrl }, CONFIGS), frameworkConfigs) 182 | } 183 | exports.getConfig = getConfig 184 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { value: true }) 2 | exports.getInjection = exports.getCodeZipPath = exports.validateTraffic = exports.removeAppid = exports.getDefaultServiceDescription = exports.getDefaultServiceName = exports.getDefaultFunctionName = exports.getDefaultProtocol = exports.capitalString = exports.getType = exports.deepClone = exports.generateId = exports.sleep = void 0 3 | const error_1 = require('tencent-component-toolkit/lib/utils/error') 4 | const download = require('download') 5 | const fse = require('fs-extra') 6 | const path = require('path') 7 | const AdmZip = require('adm-zip') 8 | const config_1 = require('./config') 9 | const CONFIGS = config_1.getConfig() 10 | function sleep(ms) { 11 | return new Promise((resolve) => { 12 | setTimeout(() => { 13 | resolve(true) 14 | }, ms) 15 | }) 16 | } 17 | exports.sleep = sleep 18 | const generateId = () => 19 | Math.random() 20 | .toString(36) 21 | .substring(6) 22 | exports.generateId = generateId 23 | const deepClone = (obj) => { 24 | return JSON.parse(JSON.stringify(obj)) 25 | } 26 | exports.deepClone = deepClone 27 | const getType = (obj) => { 28 | return Object.prototype.toString.call(obj).slice(8, -1) 29 | } 30 | exports.getType = getType 31 | const capitalString = (str) => { 32 | if (str.length < 2) { 33 | return str.toUpperCase() 34 | } 35 | return `${str[0].toUpperCase()}${str.slice(1)}` 36 | } 37 | exports.capitalString = capitalString 38 | const getDefaultProtocol = (protocols) => { 39 | return String(protocols).includes('https') ? 'https' : 'http' 40 | } 41 | exports.getDefaultProtocol = getDefaultProtocol 42 | const getDefaultFunctionName = () => { 43 | return `${CONFIGS.framework}_${exports.generateId()}` 44 | } 45 | exports.getDefaultFunctionName = getDefaultFunctionName 46 | const getDefaultServiceName = () => { 47 | return 'serverless' 48 | } 49 | exports.getDefaultServiceName = getDefaultServiceName 50 | const getDefaultServiceDescription = () => { 51 | return 'Created by Serverless Component' 52 | } 53 | exports.getDefaultServiceDescription = getDefaultServiceDescription 54 | const removeAppid = (str, appid) => { 55 | const suffix = `-${appid}` 56 | if (!str || str.indexOf(suffix) === -1) { 57 | return str 58 | } 59 | return str.slice(0, -suffix.length) 60 | } 61 | exports.removeAppid = removeAppid 62 | const validateTraffic = (num) => { 63 | if (exports.getType(num) !== 'Number') { 64 | throw new error_1.ApiTypeError( 65 | `PARAMETER_${CONFIGS.framework.toUpperCase()}_TRAFFIC`, 66 | 'traffic must be a number' 67 | ) 68 | } 69 | if (num < 0 || num > 1) { 70 | throw new error_1.ApiTypeError( 71 | `PARAMETER_${CONFIGS.framework.toUpperCase()}_TRAFFIC`, 72 | 'traffic must be a number between 0 and 1' 73 | ) 74 | } 75 | return true 76 | } 77 | exports.validateTraffic = validateTraffic 78 | const generatePublicDir = (zipPath) => { 79 | const zip = new AdmZip(zipPath) 80 | const entries = zip.getEntries() 81 | const [entry] = entries.filter((e) => e.entryName === 'app/public/' && e.name === '') 82 | if (!entry) { 83 | const extraPublicPath = path.join(__dirname, '_fixtures/public') 84 | zip.addLocalFolder(extraPublicPath, 'app/public') 85 | zip.writeZip() 86 | } 87 | } 88 | const getCodeZipPath = async (inputs) => { 89 | var _a 90 | const { framework } = CONFIGS 91 | console.log(`Packaging ${framework} application`) 92 | // unzip source zip file 93 | let zipPath 94 | if (!((_a = inputs.code) === null || _a === void 0 ? void 0 : _a.src)) { 95 | // add default template 96 | const downloadPath = `/tmp/${exports.generateId()}` 97 | const filename = 'template' 98 | console.log(`Installing Default ${framework} App`) 99 | try { 100 | await download(CONFIGS.templateUrl, downloadPath, { 101 | filename: `${filename}.zip` 102 | }) 103 | } catch (e) { 104 | throw new error_1.ApiTypeError(`DOWNLOAD_TEMPLATE`, 'Download default template failed.') 105 | } 106 | zipPath = `${downloadPath}/${filename}.zip` 107 | } else { 108 | zipPath = inputs.code.src 109 | } 110 | // 自动注入 public 目录 111 | if (framework === 'egg') { 112 | generatePublicDir(zipPath) 113 | } 114 | return zipPath 115 | } 116 | exports.getCodeZipPath = getCodeZipPath 117 | const modifyDjangoEntryFile = (projectName, shimPath) => { 118 | console.log(`Modifying django entry file for project ${projectName}`) 119 | const compShimsPath = `/tmp/_shims` 120 | const fixturePath = path.join(__dirname, '_fixtures/python') 121 | fse.copySync(shimPath, compShimsPath) 122 | fse.copySync(fixturePath, compShimsPath) 123 | // replace {{django_project}} in _shims/index.py to djangoProjectName 124 | const indexPath = path.join(compShimsPath, 'sl_handler.py') 125 | const indexPyFile = fse.readFileSync(indexPath, 'utf8') 126 | const replacedFile = indexPyFile.replace(eval('/{{django_project}}/g'), projectName) 127 | fse.writeFileSync(indexPath, replacedFile) 128 | return compShimsPath 129 | } 130 | const getDirFiles = (dirPath) => { 131 | const targetPath = path.resolve(dirPath) 132 | const files = fse.readdirSync(targetPath) 133 | const temp = {} 134 | files.forEach((file) => { 135 | temp[file] = path.join(targetPath, file) 136 | }) 137 | return temp 138 | } 139 | const getInjection = (instance, inputs) => { 140 | const { framework } = CONFIGS 141 | let injectFiles = {} 142 | let injectDirs = {} 143 | const shimPath = path.join(__dirname, '_shims') 144 | if (CONFIGS.injectSlsSdk) { 145 | injectFiles = instance.getSDKEntries(`_shims/handler.handler`) 146 | injectDirs = { 147 | _shims: shimPath 148 | } 149 | } else if (framework === 'django') { 150 | const djangoShimPath = modifyDjangoEntryFile(inputs.projectName, shimPath) 151 | injectDirs = { 152 | '': djangoShimPath 153 | } 154 | } else if (framework === 'flask') { 155 | injectDirs = { 156 | '': path.join(__dirname, '_fixtures/python') 157 | } 158 | injectFiles = getDirFiles(shimPath) 159 | } else { 160 | injectFiles = getDirFiles(shimPath) 161 | } 162 | return { injectFiles, injectDirs } 163 | } 164 | exports.getInjection = getInjection 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️⚠️⚠️ 所有框架组件项目迁移到 [tencent-framework-components](https://github.com/serverless-components/tencent-framework-components). 2 | 3 | [![Serverless Nextjs Tencent Cloud](https://img.serverlesscloud.cn/2020224/1582553715762-next.js_%E9%95%BF.png)](http://serverless.com) 4 | 5 |   6 | 7 | # 腾讯云 Next.js Serverless Component 8 | 9 | ## 简介 10 | 11 | **腾讯云[Next.js](https://github.com/zeit/next.js) 组件** - 通过使用[**Tencent Serverless Framework**](https://github.com/serverless/components/tree/cloud) , 基于云上 Serverless 服务(如 API 网关、云函数等),实现“0”配置,便捷开发,极速部署采用 Next.js 框架的网页应用,Next.js 组件支持丰富的配置扩展,提供了目前便捷实用,开发成本低的网页应用项目的开发/托管能力。 12 | 13 | 特性介绍: 14 | 15 | - [x] **按需付费** - 按照请求的使用量进行收费,没有请求时无需付费 16 | - [x] **"0"配置** - 只需要关心项目代码,之后部署即可,Serverless Framework 会搞定所有配置。 17 | - [x] **极速部署** - 部署速度快,仅需几秒,部署你的整个应用。 18 | - [x] **实时日志** - 通过实时日志的输出查看业务状态,便于直接在云端开发应用。 19 | - [x] **云端调试** - 可在云端直接进行项目调试,从而避免本地环境的差异。 20 | - [x] **便捷协作** - 通过云端控制台的状态信息和部署日志,方便进行多人协作开发。 21 | 22 | ## 快速开始 23 | 24 | 0. [**准备**](#0-准备) 25 | 1. [**安装**](#1-安装) 26 | 1. [**配置**](#2-配置) 27 | 1. [**部署**](#3-部署) 28 | 1. [**开发调试**](#4-开发调试) 29 | 1. [**查看状态**](#5-查看部署状态) 30 | 1. [**移除**](#6-移除) 31 | 32 | 更多资源: 33 | 34 | - [**账号配置**](#账号配置) 35 | - [**架构说明**](#架构说明) 36 | - [**更多组件**](#更多组件) 37 | - [**FAQ**](#FAQ) 38 | 39 | ### 1. 安装 40 | 41 | 通过 npm 全局安装 [serverless cli](https://github.com/serverless/serverless) 42 | 43 | ```bash 44 | $ npm install -g serverless 45 | ``` 46 | 47 | ### 2. 创建 48 | 49 | 通过如下命令和模板链接,快速创建一个 Next.js 应用: 50 | 51 | ```bash 52 | $ serverless init nextjs-starter --name example 53 | $ cd example 54 | ``` 55 | 56 | ### 3. 部署 57 | 58 | 在 `serverless.yml` 文件所在的项目根目录,运行以下指令,将会弹出二维码,直接扫码授权进行部署: 59 | 60 | ``` 61 | serverless deploy 62 | ``` 63 | 64 | > **说明**:如果鉴权失败,请参考 [权限配置](https://cloud.tencent.com/document/product/1154/43006) 进行授权。 65 | 66 | ### 4. 配置 67 | 68 | nextjs 组件支持 0 配置部署,也就是可以直接通过配置文件中的默认值进行部署。但你依然可以修改更多可选配置来进一步开发该 nextjs 项目。 69 | 70 | 以下是 nextjs 组件的 `serverless.yml`配置示例: 71 | 72 | ```yml 73 | # serverless.yml 74 | component: nextjs # (必填) 组件名称,此处为nextjs 75 | name: nextjsDemo # (必填) 实例名称 76 | org: orgDemo # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid 77 | app: appDemo # (可选) 该 next.js 应用名称 78 | stage: dev # (可选) 用于区分环境信息,默认值是 dev 79 | 80 | inputs: 81 | src: 82 | src: ./ 83 | exclude: 84 | - .env 85 | functionName: nextjsDemo 86 | region: ap-guangzhou 87 | runtime: Nodejs10.15 88 | apigatewayConf: 89 | protocols: 90 | - http 91 | - https 92 | environment: release 93 | ``` 94 | 95 | - 点此查看[更多配置及说明](/docs/configure.md) 96 | 97 | ### 5. 开发调试 98 | 99 | 部署了 Next.js 应用后,可以通过开发调试能力对该项目进行二次开发,从而开发一个生产应用。在本地修改和更新代码后,不需要每次都运行 `serverless deploy` 命令来反复部署。你可以直接通过 `serverless dev` 命令对本地代码的改动进行检测和自动上传。 100 | 101 | 可以通过在 `serverless.yml`文件所在的目录下运行 `serverless dev` 命令开启开发调试能力。 102 | 103 | `serverless dev` 同时支持实时输出云端日志,每次部署完毕后,对项目进行访问,即可在命令行中实时输出调用日志,便于查看业务情况和排障。 104 | 105 | ### 6. 查看部署状态 106 | 107 | 在`serverless.yml`文件所在的目录下,通过如下命令查看部署状态: 108 | 109 | ``` 110 | $ serverless info 111 | ``` 112 | 113 | ### 6. 移除 114 | 115 | 在`serverless.yml`文件所在的目录下,通过以下命令移除部署通过以下命令移除部署的 API 网关,移除后该组件会对应删除云上部署时所创建的所有相关资源。 116 | 117 | ```bash 118 | $ serverless remove 119 | ``` 120 | 121 | 和部署类似,支持通过 `serverless remove --debug` 命令查看移除过程中的实时日志信息。 122 | 123 | ### 账号配置 124 | 125 | 当前默认支持 CLI 扫描二维码登录,如您希望配置持久的环境变量/秘钥信息,也可以本地创建 `.env` 文件 126 | 127 | ```bash 128 | $ touch .env # 腾讯云的配置信息 129 | ``` 130 | 131 | 在 `.env` 文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存 132 | 133 | 如果没有腾讯云账号,可以在此 [注册新账号](https://cloud.tencent.com/register)。 134 | 135 | 如果已有腾讯云账号,可以在 [API 密钥管理](https://console.cloud.tencent.com/cam/capi) 中获取 `SecretId` 和`SecretKey`. 136 | 137 | ```text 138 | # .env 139 | TENCENT_SECRET_ID=123 140 | TENCENT_SECRET_KEY=123 141 | ``` 142 | 143 | > 注意:海外 ip 登录时,需要在`.env`文件中添加`SERVERLESS_PLATFORM_VENDOR=tencent` ,使 serverless 默认使用 tencent 组件 144 | 145 | ## 架构说明 146 | 147 | Next.js 组件将在腾讯云账户中使用到如下 Serverless 服务: 148 | 149 | - [x] **API 网关** - API 网关将会接收外部请求并且转发到 SCF 云函数中。 150 | - [x] **SCF 云函数** - 云函数将承载 Next.js 应用。 151 | - [x] **CAM 访问控制** - 该组件会创建默认 CAM 角色用于授权访问关联资源。 152 | - [x] **COS 对象存储** - 为确保上传速度和质量,云函数压缩并上传代码时,会默认将代码包存储在特定命名的 COS 桶中 153 | - [x] **SSL 证书服务** - 如果你在 yaml 文件中配置了 `apigatewayConf.customDomains` 字段,需要做自定义域名绑定并开启 HTTPS 时,也会用到证书管理服务和域名服务。Serverless Framework 会根据已经备案的域名自动申请并配置 SSL 证书。 154 | 155 | ## 更多组件 156 | 157 | 可以在 [Serverless Components](https://github.com/serverless/components) 仓库中查询更多组件的信息。 158 | 159 | ## 项目迁移 160 | 161 | 如果项目使用了自定义 Node.js 服务,比如 express 或者 koa,你需要做如下改造工作。 162 | 163 | ### 自定义 express 服务 164 | 165 | 如果你的 Next.js 项目本身运行就是基于 `express` 自定义服务的,那么你需要在项目中自定义入口文件 `sls.js`,需要参考你的服务启动文件进行修改,以下是一个模板文件: 166 | 167 | ```js 168 | const express = require('express') 169 | const next = require('next') 170 | 171 | // not report route for custom monitor 172 | const noReportRoutes = ['/_next', '/static', '/favicon.ico'] 173 | 174 | async function createServer() { 175 | const app = next({ dev: false }) 176 | const handle = app.getRequestHandler() 177 | 178 | await app.prepare() 179 | 180 | const server = express() 181 | server.all('*', (req, res) => { 182 | noReportRoutes.forEach((route) => { 183 | if (req.path.indexOf(route) !== -1) { 184 | req.__SLS_NO_REPORT__ = true 185 | } 186 | }) 187 | return handle(req, res) 188 | }) 189 | 190 | // define binary type for response 191 | // if includes, will return base64 encoded, very useful for images 192 | server.binaryTypes = ['*/*'] 193 | 194 | return server 195 | } 196 | 197 | module.exports = createServer 198 | ``` 199 | 200 | ### 自定义 koa 服务 201 | 202 | 如果你的项目使用的是 Koa 作为 Node.js 服务,需要在项目中自定义入口文件 `sls.js`,需要参考你的服务启动文件进行修改,以下是一个模板文件: 203 | 204 | ```js 205 | const Koa = require('koa') 206 | const next = require('next') 207 | 208 | async function createServer() { 209 | const app = next({ dev: false }) 210 | const handle = app.getRequestHandler() 211 | 212 | const server = new Koa() 213 | server.use((ctx) => { 214 | ctx.status = 200 215 | ctx.respond = false 216 | ctx.req.ctx = ctx 217 | 218 | return handle(ctx.req, ctx.res) 219 | }) 220 | 221 | // define binary type for response 222 | // if includes, will return base64 encoded, very useful for images 223 | server.binaryTypes = ['*/*'] 224 | 225 | return server 226 | } 227 | 228 | module.exports = createServer 229 | ``` 230 | 231 | ## 自定义监控 232 | 233 | 当在部署 Next.js 应用时,如果 `serverless.yml` 中未指定 `role`,默认会尝试绑定 `QCS_SCFExcuteRole`,并且开启自定义监控,帮助用户收集应用监控指标。对于为自定义入口文件的项目,会默认上报除含有 `/_next`、`/static` 和 `/favicon.ico` 的路由。如果你想自定义上报自己的路由性能,那么可以自定义 `sls.js` 入口文件,对于无需上报的路由,在 express 服务的 `req` 对象上添加 `__SLS_NO_REPORT__` 属性值为 `true` 即可。比如: 234 | 235 | ```js 236 | server.get('/no-report', (req, res) => { 237 | req.__SLS_NO_REPORT__ = true 238 | return handle(req, res) 239 | }) 240 | ``` 241 | 242 | 那么用户在访问 `GET /no-report` 路由时,就不会上报自定义监控指标。 243 | 244 | ## 文件上传 245 | 246 | [文件上传教程](https://github.com/serverless-components/tencent-nextjs/tree/master/docs/upload.md) 247 | 248 | ## License 249 | 250 | MIT License 251 | 252 | Copyright (c) 2020 Tencent Cloud, Inc. 253 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.2.1](https://github.com/serverless-components/tencent-nextjs/compare/v0.2.0...v0.2.1) (2021-01-26) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * change cos access by policy ([677c524](https://github.com/serverless-components/tencent-nextjs/commit/677c524868b736ea9bc839e1e235c4d46d58f780)) 7 | 8 | # [0.2.0](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.10...v0.2.0) (2021-01-26) 9 | 10 | 11 | ### Features 12 | 13 | * support apigw base64 encode ([444567e](https://github.com/serverless-components/tencent-nextjs/commit/444567e129da21d3440849e037df49e6925b6f76)) 14 | 15 | ## [0.1.10](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.9...v0.1.10) (2020-12-21) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * update remove flow ([#18](https://github.com/serverless-components/tencent-nextjs/issues/18)) ([a343607](https://github.com/serverless-components/tencent-nextjs/commit/a34360709ee03c35027a349956a7ed59b56464ed)) 21 | 22 | ## [0.1.9](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.8...v0.1.9) (2020-12-15) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * update to deployment to serial flow ([f96518a](https://github.com/serverless-components/tencent-nextjs/commit/f96518a4f2f3254f7b7c2c74e8cb3e383e176595)) 28 | 29 | ## [0.1.8](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.7...v0.1.8) (2020-10-20) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * support replace deployment and cos assets ([85e9b7c](https://github.com/serverless-components/tencent-nextjs/commit/85e9b7c7dea22fb1eb991de4aca5cfcedc6035a7)) 35 | 36 | ## [0.1.7](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.6...v0.1.7) (2020-10-12) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * support all parameters for apigw ([5e22cf3](https://github.com/serverless-components/tencent-nextjs/commit/5e22cf3595c1adfcf84de008d7af94bfc1bc4184)) 42 | 43 | ## [0.1.6](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.5...v0.1.6) (2020-10-10) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * support all configs for scf ([52b8f51](https://github.com/serverless-components/tencent-nextjs/commit/52b8f51b1cebcf8d16a658454256f4f6014f7e0b)) 49 | 50 | ## [0.1.5](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.4...v0.1.5) (2020-09-29) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * support customize sls entry file ([eb5d6b3](https://github.com/serverless-components/tencent-nextjs/commit/eb5d6b3c2406ce7a225296d3e541f1efda029dd5)) 56 | 57 | ## [0.1.4](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.3...v0.1.4) (2020-09-25) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * support koa server ([fd37c09](https://github.com/serverless-components/tencent-nextjs/commit/fd37c09412b5e123840e841289404055814d5bec)) 63 | 64 | ## [0.1.3](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.2...v0.1.3) (2020-09-23) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * update deps ([85ce058](https://github.com/serverless-components/tencent-nextjs/commit/85ce05849638ef0a318915aa39cdbc5a6ae88133)) 70 | 71 | ## [0.1.2](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.1...v0.1.2) (2020-09-03) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * update deps ([b98c0f6](https://github.com/serverless-components/tencent-nextjs/commit/b98c0f653b58fb9a45f1a6baaa5e0a70e47d3b12)) 77 | 78 | ## [0.1.1](https://github.com/serverless-components/tencent-nextjs/compare/v0.1.0...v0.1.1) (2020-09-01) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * update tencent-component-toolkit ([7642596](https://github.com/serverless-components/tencent-nextjs/commit/7642596228f2972cd857096f4d30a31483175175)) 84 | 85 | # [0.1.0](https://github.com/serverless-components/tencent-nextjs/compare/v0.0.14...v0.1.0) (2020-08-28) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * update deps for uniform error message ([b2a0ea4](https://github.com/serverless-components/tencent-nextjs/commit/b2a0ea423e89a1ec4694bb8fae11b881412b6f36)) 91 | 92 | 93 | ### Features 94 | 95 | * optimize deploy flow and update deps ([6c34a4d](https://github.com/serverless-components/tencent-nextjs/commit/6c34a4d1d8f74be5f55aa7a1659f5786b366771b)) 96 | 97 | ## [0.0.14](https://github.com/serverless-components/tencent-nextjs/compare/v0.0.13...v0.0.14) (2020-08-26) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * add no report for favicon ([28484c4](https://github.com/serverless-components/tencent-nextjs/commit/28484c48e6011004df81b1177dd19c03f4b077a1)) 103 | * apigw custom domain update bug ([9f856b5](https://github.com/serverless-components/tencent-nextjs/commit/9f856b565e67a4656d041e5f0d277f759f3c01ff)) 104 | * apigw metics x timestamp ([d68a581](https://github.com/serverless-components/tencent-nextjs/commit/d68a581e02d267871191d16d010c34f1798b0735)) 105 | * cache http server ([8e01009](https://github.com/serverless-components/tencent-nextjs/commit/8e01009823fc1682f75f17e7061b9934b03d5f38)) 106 | * change staticCdn to staticConf ([e481a94](https://github.com/serverless-components/tencent-nextjs/commit/e481a9492f1c9f7dba0c168fce0be73fa61cd0f4)) 107 | * component name ([2f45508](https://github.com/serverless-components/tencent-nextjs/commit/2f455081075379f209600a200b44bd91a50d2658)) 108 | * lambdaArn value ([0f63bda](https://github.com/serverless-components/tencent-nextjs/commit/0f63bdae851f6b3ef14c141ee23fd4d2507f54bb)) 109 | * read bucket and object from srcOriginal ([c57ae19](https://github.com/serverless-components/tencent-nextjs/commit/c57ae19dd1457d6554fa0b6fc92a6984fc474262)) 110 | * static cos has suffix appid ([1bf7400](https://github.com/serverless-components/tencent-nextjs/commit/1bf74008c729ca927e9fb2a201340606f9e8d6e3)) 111 | * support apigw serviceTimeout ([3afd507](https://github.com/serverless-components/tencent-nextjs/commit/3afd507a4b72bf5113322ead4d950df6a3a717f2)) 112 | * support eip config ([fa0358b](https://github.com/serverless-components/tencent-nextjs/commit/fa0358b85f2ca9af1abe86e1c815c7d9e3ef63ac)) 113 | * throw error when no temp secrets ([fe0b6b0](https://github.com/serverless-components/tencent-nextjs/commit/fe0b6b061bec839db8022d3d0f36fee89dd7a146)) 114 | * traffic zero display bug ([6e37e82](https://github.com/serverless-components/tencent-nextjs/commit/6e37e824e6e6009dc1fb952ce3e4ed1b38eb8564)) 115 | * typo ([2d26ef7](https://github.com/serverless-components/tencent-nextjs/commit/2d26ef732a97f3e56ef9eb96d6b849fea76eaff4)) 116 | * update deps ([28c91d4](https://github.com/serverless-components/tencent-nextjs/commit/28c91d45ebce353e953e8fcafcb66f0e56419865)) 117 | * update error message ([6a2454e](https://github.com/serverless-components/tencent-nextjs/commit/6a2454eb2bf7b9cd14ff2c96db051e6c674a5b0f)) 118 | * update get credential error message ([089a197](https://github.com/serverless-components/tencent-nextjs/commit/089a197ae7bc8e7cc10dcb8d97d160d8213c2145)) 119 | * update toolkit verison ([33fe814](https://github.com/serverless-components/tencent-nextjs/commit/33fe81494992bf2ab1b7530e15fe0f179e212f9d)) 120 | * upgrade deps ([c8920cc](https://github.com/serverless-components/tencent-nextjs/commit/c8920cc02fc478b32a44a26762dd0dcfd131d94b)) 121 | * upgrade deps ([f2a5fda](https://github.com/serverless-components/tencent-nextjs/commit/f2a5fdaa9f05ffc8fb0437a24a4a15db8b9bbc50)) 122 | * upgrade tencent-component-toolkit for deleting compatibility ([ddb89ae](https://github.com/serverless-components/tencent-nextjs/commit/ddb89ae5bd6080f08cef5e489307703e7760be1f)) 123 | * upgrade tencent-serverless-http ([706be01](https://github.com/serverless-components/tencent-nextjs/commit/706be018d88cfb93a8a066d14ab99581f6fff372)) 124 | 125 | 126 | ### Features 127 | 128 | * add base metrics for next.js ([d1f11e6](https://github.com/serverless-components/tencent-nextjs/commit/d1f11e6a185d826f41e2ec46178e0639265ef39b)) 129 | * download remote template for default deploy ([fd3b3c4](https://github.com/serverless-components/tencent-nextjs/commit/fd3b3c48fc8a227cf40d7a33dc47429b88aadd11)) 130 | * make callbackWaitsForEmptyEventLoop default to false ([84929c2](https://github.com/serverless-components/tencent-nextjs/commit/84929c280e0f0c3a51348d627d053da8bf9b4d08)) 131 | * optimize deploy log ([4a07cc1](https://github.com/serverless-components/tencent-nextjs/commit/4a07cc162a4885a776845f3da6a17b6e5e02ec73)) 132 | * optimize zip flow ([9490df1](https://github.com/serverless-components/tencent-nextjs/commit/9490df168b0f28f3de9d01544f67570ae86df654)) 133 | * rename _nexjts to _shims ([a0d9b33](https://github.com/serverless-components/tencent-nextjs/commit/a0d9b33185f9185b37491ff0dd0ee0cf984d0fed)) 134 | * support api gw metrics ([1c0f204](https://github.com/serverless-components/tencent-nextjs/commit/1c0f2045bc5c8b7ccba2f6a0226352aa1198a895)) 135 | * support disable creating apigw ([430e46e](https://github.com/serverless-components/tencent-nextjs/commit/430e46e87ec9650565bd23a9fab39bd4e1d6dc2d)) 136 | * support gray release ([bfa6bb7](https://github.com/serverless-components/tencent-nextjs/commit/bfa6bb7c79ae7ba7a32fbf42bb9315b77b77259f)) 137 | * support layers config ([6c82ffd](https://github.com/serverless-components/tencent-nextjs/commit/6c82ffdae8121daf8b7718d718d55ef751e06b1b)) 138 | * support static cdn deployment ([4a5d93a](https://github.com/serverless-components/tencent-nextjs/commit/4a5d93a216eeebc2dbe323ded0e259212ff709b0)) 139 | * update config & support usageplan+auth ([59920dc](https://github.com/serverless-components/tencent-nextjs/commit/59920dc934994984fb46c693afc6d4d5adafb007)) 140 | -------------------------------------------------------------------------------- /src/formatter.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { value: true }) 2 | exports.formatInputs = exports.formatStaticCdnInputs = exports.formatStaticCosInputs = void 0 3 | const AdmZip = require('adm-zip') 4 | const error_1 = require('tencent-component-toolkit/lib/utils/error') 5 | const config_1 = require('./config') 6 | const CONFIGS = config_1.getConfig() 7 | const utils_1 = require('./utils') 8 | const formatStaticCosInputs = async (cosConf, appId, codeZipPath, region) => { 9 | try { 10 | const staticCosInputs = [] 11 | const sources = cosConf.sources || CONFIGS.defaultStatics 12 | const { bucket } = cosConf 13 | // 删除用户填写时携带的 appid 14 | const bucketName = utils_1.removeAppid(bucket, appId) 15 | const staticPath = `/tmp/${utils_1.generateId()}` 16 | const codeZip = new AdmZip(codeZipPath) 17 | const entries = codeZip.getEntries() 18 | for (let i = 0; i < sources.length; i++) { 19 | const curSource = sources[i] 20 | const entryName = `${curSource.src}` 21 | let exist = false 22 | entries.forEach((et) => { 23 | if (et.entryName.indexOf(entryName) === 0) { 24 | codeZip.extractEntryTo(et, staticPath, true, true) 25 | exist = true 26 | } 27 | }) 28 | if (exist) { 29 | const cosInputs = { 30 | force: true, 31 | protocol: 'https', 32 | bucket: `${bucketName}-${appId}`, 33 | src: `${staticPath}/${entryName}`, 34 | keyPrefix: curSource.targetDir || '/' 35 | } 36 | staticCosInputs.push(cosInputs) 37 | } 38 | } 39 | return { 40 | bucket: `${bucketName}-${appId}`, 41 | staticCosInputs, 42 | // 通过设置 policy 来支持公网访问 43 | policy: CONFIGS.getPolicy(region, `${bucketName}-${appId}`, appId) 44 | } 45 | } catch (e) { 46 | throw new error_1.ApiTypeError( 47 | `UTILS_${CONFIGS.framework.toUpperCase()}_prepareStaticCosInputs`, 48 | e.message, 49 | e.stack 50 | ) 51 | } 52 | } 53 | exports.formatStaticCosInputs = formatStaticCosInputs 54 | const formatStaticCdnInputs = async (cdnConf, origin) => { 55 | const cdnInputs = { 56 | async: true, 57 | area: cdnConf.area || 'mainland', 58 | domain: cdnConf.domain, 59 | serviceType: 'web', 60 | origin: { 61 | origins: [origin], 62 | originType: 'cos', 63 | originPullProtocol: 'https' 64 | }, 65 | onlyRefresh: cdnConf.onlyRefresh 66 | } 67 | if (cdnConf.https) { 68 | // 通过提供默认的配置来简化用户配置 69 | cdnInputs.forceRedirect = cdnConf.forceRedirect || CONFIGS.defaultCdnConfig.forceRedirect 70 | if (!cdnConf.https.certId) { 71 | throw new error_1.ApiTypeError( 72 | `PARAMETER_${CONFIGS.framework.toUpperCase()}_HTTPS`, 73 | 'https.certId is required' 74 | ) 75 | } 76 | cdnInputs.https = Object.assign(Object.assign({}, CONFIGS.defaultCdnConfig.https), { 77 | http2: cdnConf.https.http2 || 'on', 78 | certInfo: { 79 | certId: cdnConf.https.certId 80 | } 81 | }) 82 | } 83 | if (cdnConf.autoRefresh !== false) { 84 | cdnInputs.refreshCdn = { 85 | flushType: cdnConf.refreshType || 'delete', 86 | urls: [`http://${cdnInputs.domain}`, `https://${cdnInputs.domain}`] 87 | } 88 | } 89 | return cdnInputs 90 | } 91 | exports.formatStaticCdnInputs = formatStaticCdnInputs 92 | const formatInputs = (state, inputs = {}) => { 93 | var _a, 94 | _b, 95 | _c, 96 | _d, 97 | _e, 98 | _f, 99 | _g, 100 | _h, 101 | _j, 102 | _k, 103 | _l, 104 | _m, 105 | _o, 106 | _p, 107 | _q, 108 | _r, 109 | _s, 110 | _t, 111 | _u, 112 | _v, 113 | _w, 114 | _x, 115 | _y, 116 | _z, 117 | _0, 118 | _1, 119 | _2, 120 | _3, 121 | _4, 122 | _5, 123 | _6, 124 | _7, 125 | _8, 126 | _9, 127 | _10, 128 | _11, 129 | _12, 130 | _13 131 | // 标准化函数参数 132 | const tempFunctionConf = (_a = inputs.functionConf) !== null && _a !== void 0 ? _a : {} 133 | const region = (_b = inputs.region) !== null && _b !== void 0 ? _b : 'ap-guangzhou' 134 | // 获取状态中的函数名称 135 | const regionState = state[region] 136 | const stateFunctionName = state.functionName || (regionState && regionState.funcitonName) 137 | const functionConf = Object.assign(tempFunctionConf, { 138 | code: { 139 | src: inputs.src, 140 | bucket: 141 | (_c = inputs === null || inputs === void 0 ? void 0 : inputs.srcOriginal) === null || 142 | _c === void 0 143 | ? void 0 144 | : _c.bucket, 145 | object: 146 | (_d = inputs === null || inputs === void 0 ? void 0 : inputs.srcOriginal) === null || 147 | _d === void 0 148 | ? void 0 149 | : _d.object 150 | }, 151 | name: 152 | (_g = 153 | (_f = (_e = tempFunctionConf.name) !== null && _e !== void 0 ? _e : inputs.functionName) !== 154 | null && _f !== void 0 155 | ? _f 156 | : stateFunctionName) !== null && _g !== void 0 157 | ? _g 158 | : utils_1.getDefaultFunctionName(), 159 | region: region, 160 | role: 161 | (_j = (_h = tempFunctionConf.role) !== null && _h !== void 0 ? _h : inputs.role) !== null && 162 | _j !== void 0 163 | ? _j 164 | : '', 165 | handler: 166 | (_l = (_k = tempFunctionConf.handler) !== null && _k !== void 0 ? _k : inputs.handler) !== 167 | null && _l !== void 0 168 | ? _l 169 | : CONFIGS.handler, 170 | runtime: 171 | (_o = (_m = tempFunctionConf.runtime) !== null && _m !== void 0 ? _m : inputs.runtime) !== 172 | null && _o !== void 0 173 | ? _o 174 | : CONFIGS.runtime, 175 | namespace: 176 | (_q = (_p = tempFunctionConf.namespace) !== null && _p !== void 0 ? _p : inputs.namespace) !== 177 | null && _q !== void 0 178 | ? _q 179 | : CONFIGS.namespace, 180 | description: 181 | (_s = 182 | (_r = tempFunctionConf.description) !== null && _r !== void 0 ? _r : inputs.description) !== 183 | null && _s !== void 0 184 | ? _s 185 | : CONFIGS.description, 186 | layers: 187 | (_u = (_t = tempFunctionConf.layers) !== null && _t !== void 0 ? _t : inputs.layers) !== 188 | null && _u !== void 0 189 | ? _u 190 | : [], 191 | cfs: (_v = tempFunctionConf.cfs) !== null && _v !== void 0 ? _v : [], 192 | publish: tempFunctionConf.publish || inputs.publish, 193 | traffic: tempFunctionConf.traffic || inputs.traffic, 194 | lastVersion: state.lastVersion, 195 | timeout: (_w = tempFunctionConf.timeout) !== null && _w !== void 0 ? _w : CONFIGS.timeout, 196 | memorySize: 197 | (_x = tempFunctionConf.memorySize) !== null && _x !== void 0 ? _x : CONFIGS.memorySize, 198 | tags: 199 | (_z = (_y = tempFunctionConf.tags) !== null && _y !== void 0 ? _y : inputs.tags) !== null && 200 | _z !== void 0 201 | ? _z 202 | : null 203 | }) 204 | if (!((_0 = functionConf.environment) === null || _0 === void 0 ? void 0 : _0.variables)) { 205 | functionConf.environment = { 206 | variables: {} 207 | } 208 | } 209 | // 添加框架需要添加的默认环境变量 210 | const { defaultEnvs } = CONFIGS 211 | defaultEnvs.forEach((item) => { 212 | functionConf.environment.variables[item.key] = item.value 213 | }) 214 | // 添加入口文件环境变量 215 | const entryFile = functionConf.entryFile || inputs.entryFile || CONFIGS.defaultEntryFile 216 | if (entryFile) { 217 | functionConf.environment.variables['SLS_ENTRY_FILE'] = entryFile 218 | } 219 | // django 项目需要 projectName 参数 220 | if (CONFIGS.framework === 'django') { 221 | functionConf.projectName = 222 | (_3 = 223 | (_2 = 224 | (_1 = tempFunctionConf.projectName) !== null && _1 !== void 0 225 | ? _1 226 | : tempFunctionConf.djangoProjectName) !== null && _2 !== void 0 227 | ? _2 228 | : inputs.djangoProjectName) !== null && _3 !== void 0 229 | ? _3 230 | : '' 231 | } 232 | // TODO: 验证流量配置,将废弃 233 | if (inputs.traffic !== undefined) { 234 | utils_1.validateTraffic(inputs.traffic) 235 | } 236 | // TODO: 判断是否需要配置流量,将废弃 237 | functionConf.needSetTraffic = inputs.traffic !== undefined && functionConf.lastVersion 238 | // 初始化 VPC 配置,兼容旧的vpc配置 239 | const vpc = tempFunctionConf.vpcConfig || tempFunctionConf.vpc || inputs.vpcConfig || inputs.vpc 240 | if (vpc) { 241 | functionConf.vpcConfig = vpc 242 | } 243 | // 标准化网关配置参数 244 | const tempApigwConf = (_4 = inputs.apigatewayConf) !== null && _4 !== void 0 ? _4 : {} 245 | const apigatewayConf = Object.assign(tempApigwConf, { 246 | serviceId: 247 | (_6 = (_5 = tempApigwConf.serviceId) !== null && _5 !== void 0 ? _5 : tempApigwConf.id) !== 248 | null && _6 !== void 0 249 | ? _6 250 | : inputs.serviceId, 251 | region: region, 252 | isDisabled: tempApigwConf.isDisabled === true, 253 | serviceName: 254 | (_9 = 255 | (_8 = 256 | (_7 = tempApigwConf.serviceName) !== null && _7 !== void 0 ? _7 : tempApigwConf.name) !== 257 | null && _8 !== void 0 258 | ? _8 259 | : inputs.serviceName) !== null && _9 !== void 0 260 | ? _9 261 | : utils_1.getDefaultServiceName(), 262 | serviceDesc: 263 | (_11 = 264 | (_10 = tempApigwConf.serviceDesc) !== null && _10 !== void 0 265 | ? _10 266 | : tempApigwConf.description) !== null && _11 !== void 0 267 | ? _11 268 | : utils_1.getDefaultServiceDescription(), 269 | protocols: tempApigwConf.protocols || ['http'], 270 | environment: tempApigwConf.environment ? tempApigwConf.environment : 'release', 271 | customDomains: tempApigwConf.customDomains || [] 272 | }) 273 | // 如果没配置,添加默认的 API 配置,通常 Web 框架组件是不要用户自定义的 274 | if (!apigatewayConf.endpoints) { 275 | apigatewayConf.endpoints = [ 276 | { 277 | path: tempApigwConf.path || '/', 278 | enableCORS: 279 | (_12 = tempApigwConf.enableCORS) !== null && _12 !== void 0 ? _12 : tempApigwConf.cors, 280 | serviceTimeout: 281 | (_13 = tempApigwConf.serviceTimeout) !== null && _13 !== void 0 282 | ? _13 283 | : tempApigwConf.timeout, 284 | method: tempApigwConf.method || 'ANY', 285 | apiName: tempApigwConf.apiName || 'index', 286 | isBase64Encoded: tempApigwConf.isBase64Encoded, 287 | function: { 288 | isIntegratedResponse: true, 289 | functionName: functionConf.name, 290 | functionNamespace: functionConf.namespace, 291 | functionQualifier: 292 | (tempApigwConf.function && tempApigwConf.function.functionQualifier) || 293 | apigatewayConf.qualifier || 294 | '$DEFAULT' 295 | } 296 | } 297 | ] 298 | } 299 | if (tempApigwConf.usagePlan) { 300 | apigatewayConf.endpoints[0].usagePlan = { 301 | usagePlanId: tempApigwConf.usagePlan.usagePlanId, 302 | usagePlanName: tempApigwConf.usagePlan.usagePlanName, 303 | usagePlanDesc: tempApigwConf.usagePlan.usagePlanDesc, 304 | maxRequestNum: tempApigwConf.usagePlan.maxRequestNum 305 | } 306 | } 307 | if (tempApigwConf.auth) { 308 | apigatewayConf.endpoints[0].auth = { 309 | secretName: tempApigwConf.auth.secretName, 310 | secretIds: tempApigwConf.auth.secretIds 311 | } 312 | } 313 | return { 314 | region, 315 | functionConf, 316 | apigatewayConf 317 | } 318 | } 319 | exports.formatInputs = formatInputs 320 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { value: true }) 2 | exports.ServerlessComponent = void 0 3 | const core_1 = require('@serverless/core') 4 | const tencent_component_toolkit_1 = require('tencent-component-toolkit') 5 | const error_1 = require('tencent-component-toolkit/lib/utils/error') 6 | const utils_1 = require('./utils') 7 | const formatter_1 = require('./formatter') 8 | const config_1 = require('./config') 9 | const CONFIGS = config_1.getConfig() 10 | class ServerlessComponent extends core_1.Component { 11 | getCredentials() { 12 | const { tmpSecrets } = this.credentials.tencent 13 | if (!tmpSecrets || !tmpSecrets.TmpSecretId) { 14 | throw new error_1.ApiTypeError( 15 | 'CREDENTIAL', 16 | '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.' 17 | ) 18 | } 19 | return { 20 | SecretId: tmpSecrets.TmpSecretId, 21 | SecretKey: tmpSecrets.TmpSecretKey, 22 | Token: tmpSecrets.Token 23 | } 24 | } 25 | getAppId() { 26 | return this.credentials.tencent.tmpSecrets.appId 27 | } 28 | async uploadCodeToCos(appId, inputs, region) { 29 | var _a, _b, _c, _d, _e, _f 30 | const credentials = this.getCredentials() 31 | const bucketName = 32 | ((_a = inputs.code) === null || _a === void 0 ? void 0 : _a.bucket) || 33 | `sls-cloudfunction-${region}-code` 34 | const objectName = 35 | ((_b = inputs.code) === null || _b === void 0 ? void 0 : _b.object) || 36 | `${inputs.name}-${Math.floor(Date.now() / 1000)}.zip` 37 | // if set bucket and object not pack code 38 | if ( 39 | !((_c = inputs.code) === null || _c === void 0 ? void 0 : _c.bucket) || 40 | !((_d = inputs.code) === null || _d === void 0 ? void 0 : _d.object) 41 | ) { 42 | const zipPath = await utils_1.getCodeZipPath(inputs) 43 | console.log(`Code zip path ${zipPath}`) 44 | // save the zip path to state for lambda to use it 45 | this.state.zipPath = zipPath 46 | const cos = new tencent_component_toolkit_1.Cos(credentials, region) 47 | if (!((_e = inputs.code) === null || _e === void 0 ? void 0 : _e.bucket)) { 48 | // create default bucket 49 | await cos.deploy({ 50 | bucket: bucketName + '-' + appId, 51 | force: true, 52 | lifecycle: [ 53 | { 54 | status: 'Enabled', 55 | id: 'deleteObject', 56 | expiration: { days: '10' }, 57 | abortIncompleteMultipartUpload: { daysAfterInitiation: '10' } 58 | } 59 | ] 60 | }) 61 | } 62 | // upload code to cos 63 | if (!((_f = inputs.code) === null || _f === void 0 ? void 0 : _f.object)) { 64 | console.log(`Getting cos upload url for bucket ${bucketName}`) 65 | const uploadUrl = await cos.getObjectUrl({ 66 | bucket: bucketName + '-' + appId, 67 | object: objectName, 68 | method: 'PUT' 69 | }) 70 | // if shims and sls sdk entries had been injected to zipPath, no need to injected again 71 | console.log(`Uploading code to bucket ${bucketName}`) 72 | const { injectFiles, injectDirs } = utils_1.getInjection(this, inputs) 73 | await this.uploadSourceZipToCOS(zipPath, uploadUrl, injectFiles, injectDirs) 74 | console.log(`Upload ${objectName} to bucket ${bucketName} success`) 75 | } 76 | } 77 | // save bucket state 78 | this.state.bucket = bucketName 79 | this.state.object = objectName 80 | return { 81 | bucket: bucketName, 82 | object: objectName 83 | } 84 | } 85 | async deployFunction(credentials, inputs = {}, region) { 86 | var _a, _b, _c, _d 87 | const appId = this.getAppId() 88 | const code = await this.uploadCodeToCos(appId, inputs, region) 89 | const scf = new tencent_component_toolkit_1.Scf(credentials, region) 90 | const tempInputs = Object.assign(Object.assign({}, inputs), { code }) 91 | const scfOutput = await scf.deploy(utils_1.deepClone(tempInputs)) 92 | const outputs = { 93 | functionName: scfOutput.FunctionName, 94 | runtime: scfOutput.Runtime, 95 | namespace: scfOutput.Namespace 96 | } 97 | this.state = Object.assign(Object.assign({}, this.state), outputs) 98 | // default version is $LATEST 99 | outputs.lastVersion = 100 | (_b = 101 | (_a = scfOutput.LastVersion) !== null && _a !== void 0 ? _a : this.state.lastVersion) !== 102 | null && _b !== void 0 103 | ? _b 104 | : '$LATEST' 105 | // default traffic is 1.0, it can also be 0, so we should compare to undefined 106 | outputs.traffic = 107 | (_d = (_c = scfOutput.Traffic) !== null && _c !== void 0 ? _c : this.state.traffic) !== 108 | null && _d !== void 0 109 | ? _d 110 | : 1 111 | if (outputs.traffic !== 1 && scfOutput.ConfigTrafficVersion) { 112 | outputs.configTrafficVersion = scfOutput.ConfigTrafficVersion 113 | this.state.configTrafficVersion = scfOutput.ConfigTrafficVersion 114 | } 115 | this.state.lastVersion = outputs.lastVersion 116 | this.state.traffic = outputs.traffic 117 | return outputs 118 | } 119 | async deployApigw(credentials, inputs, region) { 120 | var _a, _b 121 | const { state } = this 122 | const serviceId = 123 | (_a = inputs.serviceId) !== null && _a !== void 0 ? _a : state && state.serviceId 124 | const apigw = new tencent_component_toolkit_1.Apigw(credentials, region) 125 | const oldState = (_b = this.state) !== null && _b !== void 0 ? _b : {} 126 | const apigwInputs = Object.assign(Object.assign({}, inputs), { 127 | oldState: { 128 | apiList: oldState.apiList || [], 129 | customDomains: oldState.customDomains || [] 130 | } 131 | }) 132 | // different region deployment has different service id 133 | apigwInputs.serviceId = serviceId 134 | const apigwOutput = await apigw.deploy(utils_1.deepClone(apigwInputs)) 135 | const outputs = { 136 | serviceId: apigwOutput.serviceId, 137 | subDomain: apigwOutput.subDomain, 138 | environment: apigwOutput.environment, 139 | url: `${utils_1.getDefaultProtocol(inputs.protocols)}://${apigwOutput.subDomain}/${ 140 | apigwOutput.environment 141 | }${apigwInputs.endpoints[0].path}` 142 | } 143 | if (apigwOutput.customDomains) { 144 | outputs.customDomains = apigwOutput.customDomains 145 | } 146 | this.state = Object.assign(Object.assign(Object.assign({}, this.state), outputs), { 147 | apiList: apigwOutput.apiList, 148 | created: true 149 | }) 150 | return outputs 151 | } 152 | // deploy static to cos, and setup cdn 153 | async deployStatic(inputs, region) { 154 | const credentials = this.getCredentials() 155 | const { zipPath } = this.state 156 | const appId = this.getAppId() 157 | const deployStaticOutputs = { 158 | cos: { 159 | region: '', 160 | cosOrigin: '' 161 | } 162 | } 163 | if (zipPath) { 164 | console.log(`Deploying static files`) 165 | // 1. deploy to cos 166 | const { staticCosInputs, bucket, policy } = await formatter_1.formatStaticCosInputs( 167 | inputs.cosConf, 168 | appId, 169 | zipPath, 170 | region 171 | ) 172 | const cos = new tencent_component_toolkit_1.Cos(credentials, region) 173 | const cosOutput = { 174 | region, 175 | bucket, 176 | cosOrigin: `${bucket}.cos.${region}.myqcloud.com`, 177 | url: `https://${bucket}.cos.${region}.myqcloud.com` 178 | } 179 | // try to create bucket 180 | await cos.createBucket({ 181 | bucket, 182 | force: true 183 | }) 184 | // set public access policy 185 | await cos.setPolicy({ 186 | bucket, 187 | policy 188 | }) 189 | // 创建 COS 桶后等待1s,防止偶发出现桶不存在错误 190 | await utils_1.sleep(1000) 191 | // flush bucket 192 | if (inputs.cosConf.replace) { 193 | await cos.flushBucketFiles(bucket) 194 | try { 195 | } catch (e) {} 196 | } 197 | for (let i = 0; i < staticCosInputs.length; i++) { 198 | const curInputs = staticCosInputs[i] 199 | console.log(`Starting upload directory ${curInputs.src} to cos bucket ${curInputs.bucket}`) 200 | await cos.upload({ 201 | bucket, 202 | dir: curInputs.src, 203 | keyPrefix: curInputs.keyPrefix 204 | }) 205 | console.log(`Upload directory ${curInputs.src} to cos bucket ${curInputs.bucket} success`) 206 | } 207 | deployStaticOutputs.cos = cosOutput 208 | // 2. deploy cdn 209 | if (inputs.cdnConf) { 210 | const cdn = new tencent_component_toolkit_1.Cdn(credentials) 211 | const cdnInputs = await formatter_1.formatStaticCdnInputs( 212 | inputs.cdnConf, 213 | cosOutput.cosOrigin 214 | ) 215 | console.log(`Starting deploy cdn ${cdnInputs.domain}`) 216 | const cdnDeployRes = await cdn.deploy(cdnInputs) 217 | const protocol = cdnInputs.https ? 'https' : 'http' 218 | const cdnOutput = { 219 | domain: cdnDeployRes.domain, 220 | url: `${protocol}://${cdnDeployRes.domain}`, 221 | cname: cdnDeployRes.cname 222 | } 223 | deployStaticOutputs.cdn = cdnOutput 224 | console.log(`Deploy cdn ${cdnInputs.domain} success`) 225 | } 226 | console.log(`Deployed static files success`) 227 | return deployStaticOutputs 228 | } 229 | return null 230 | } 231 | async deploy(inputs) { 232 | var _a 233 | console.log(`Deploying ${CONFIGS.framework} application`) 234 | const credentials = this.getCredentials() 235 | // 对Inputs内容进行标准化 236 | const { region, functionConf, apigatewayConf } = await formatter_1.formatInputs( 237 | this.state, 238 | inputs 239 | ) 240 | // 部署函数 + API网关 241 | const outputs = {} 242 | if (!((_a = functionConf.code) === null || _a === void 0 ? void 0 : _a.src)) { 243 | outputs.templateUrl = CONFIGS.templateUrl 244 | } 245 | let apigwOutputs 246 | const functionOutputs = await this.deployFunction(credentials, functionConf, region) 247 | // support apigatewayConf.isDisabled 248 | if (apigatewayConf.isDisabled !== true) { 249 | apigwOutputs = await this.deployApigw(credentials, apigatewayConf, region) 250 | } else { 251 | this.state.apigwDisabled = true 252 | } 253 | // optimize outputs for one region 254 | outputs.region = region 255 | outputs.scf = functionOutputs 256 | if (apigwOutputs) { 257 | outputs.apigw = apigwOutputs 258 | } 259 | // start deploy static cdn 260 | if (inputs.staticConf) { 261 | const { staticConf } = inputs 262 | const res = await this.deployStatic(staticConf, region) 263 | if (res) { 264 | this.state.staticConf = res 265 | outputs.staticConf = res 266 | } 267 | } 268 | this.state.region = region 269 | this.state.lambdaArn = functionConf.name 270 | return outputs 271 | } 272 | async removeStatic() { 273 | // remove static 274 | const { region, staticConf } = this.state 275 | if (staticConf) { 276 | console.log(`Removing static files`) 277 | const credentials = this.getCredentials() 278 | // 1. remove cos 279 | if (staticConf.cos) { 280 | const { cos: cosState } = staticConf 281 | if (cosState.bucket) { 282 | const { bucket } = cosState 283 | const cos = new tencent_component_toolkit_1.Cos(credentials, region) 284 | await cos.remove({ bucket }) 285 | } 286 | } 287 | // 2. remove cdn 288 | if (staticConf.cdn) { 289 | const cdn = new tencent_component_toolkit_1.Cdn(credentials) 290 | try { 291 | await cdn.remove(staticConf.cdn) 292 | } catch (e) { 293 | // no op 294 | } 295 | } 296 | console.log(`Remove static config success`) 297 | } 298 | } 299 | async remove() { 300 | console.log(`Removing application`) 301 | const { state } = this 302 | const { region } = state 303 | const { 304 | namespace, 305 | functionName, 306 | created, 307 | serviceId, 308 | apigwDisabled, 309 | customDomains, 310 | apiList, 311 | environment 312 | } = state 313 | const credentials = this.getCredentials() 314 | // if disable apigw, no need to remove 315 | if (apigwDisabled !== true && serviceId) { 316 | const apigw = new tencent_component_toolkit_1.Apigw(credentials, region) 317 | await apigw.remove({ 318 | created, 319 | environment, 320 | serviceId, 321 | apiList, 322 | customDomains 323 | }) 324 | } 325 | if (functionName) { 326 | const scf = new tencent_component_toolkit_1.Scf(credentials, region) 327 | await scf.remove({ 328 | functionName, 329 | namespace 330 | }) 331 | } 332 | // remove static 333 | await this.removeStatic() 334 | this.state = {} 335 | } 336 | async metrics(inputs = {}) { 337 | console.log(`Getting metrics data`) 338 | if (!inputs.rangeStart || !inputs.rangeEnd) { 339 | throw new error_1.ApiTypeError( 340 | `PARAMETER_${CONFIGS.framework.toUpperCase()}_METRICS`, 341 | 'rangeStart and rangeEnd are require inputs' 342 | ) 343 | } 344 | const { state } = this 345 | const { region } = state 346 | if (!region) { 347 | throw new error_1.ApiTypeError( 348 | `PARAMETER_${CONFIGS.framework.toUpperCase()}_METRICS`, 349 | 'No region property in state' 350 | ) 351 | } 352 | const { functionName, namespace } = state 353 | if (functionName) { 354 | const options = { 355 | funcName: functionName, 356 | namespace: namespace, 357 | region, 358 | timezone: inputs.tz 359 | } 360 | if (state.serviceId) { 361 | options.apigwServiceId = state.serviceId 362 | options.apigwEnvironment = state.environment || 'release' 363 | } 364 | const credentials = this.getCredentials() 365 | const mertics = new tencent_component_toolkit_1.Metrics(credentials, options) 366 | const metricResults = await mertics.getDatas( 367 | inputs.rangeStart, 368 | inputs.rangeEnd, 369 | tencent_component_toolkit_1.Metrics.Type.All 370 | ) 371 | return metricResults 372 | } 373 | throw new error_1.ApiTypeError( 374 | `PARAMETER_${CONFIGS.framework.toUpperCase()}_METRICS`, 375 | 'Function name not define' 376 | ) 377 | } 378 | } 379 | exports.ServerlessComponent = ServerlessComponent 380 | -------------------------------------------------------------------------------- /docs/configure.md: -------------------------------------------------------------------------------- 1 | # 配置文档 2 | 3 | ## 全部配置 4 | 5 | ```yml 6 | # serverless.yml 7 | 8 | component: nextjs # (必选) 组件名称,在该实例中为nextjs 9 | name: nextjsDemo # 必选) 组件实例名称. 10 | org: orgDemo # (可选) 用于记录组织信息,默认值为您的腾讯云账户 appid,必须为字符串 11 | app: appDemo # (可选) 用于记录组织信息. 默认与name相同,必须为字符串 12 | stage: dev # (可选) 用于区分环境信息,默认值是 dev 13 | 14 | inputs: 15 | region: ap-guangzhou # 云函数所在区域 16 | functionName: nextjsDemo # 云函数名称 17 | serviceName: mytest # api网关服务名称 18 | runtime: Nodejs10.15 # 运行环境 19 | serviceId: service-np1uloxw # api网关服务ID 20 | entryFile: sls.js # 自定义 server 的入口文件名,默认为 sls.js,如果不想修改文件名为 sls.js 可以自定义 21 | src: ./src # 第一种为string时,会打包src对应目录下的代码上传到默认cos上。 22 | # src: # 第二种,部署src下的文件代码,并打包成zip上传到bucket上 23 | # src: ./src # 本地需要打包的文件目录 24 | # bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid 25 | # exclude: # 被排除的文件或目录 26 | # - .env 27 | # - node_modules 28 | # src: # 第三种,在指定存储桶bucket中已经存在了object代码,直接部署 29 | # bucket: bucket01 # bucket name,当前会默认在bucket name后增加 appid 后缀, 本例中为 bucket01-appid 30 | # object: cos.zip # bucket key 指定存储桶内的文件 31 | layers: 32 | - name: layerName # layer名称 33 | version: 1 # 版本 34 | functionConf: # 函数配置相关 35 | timeout: 10 # 超时时间,单位秒 36 | eip: false # 是否固定出口IP 37 | memorySize: 128 # 内存大小,单位MB 38 | environment: # 环境变量 39 | variables: # 环境变量数组 40 | TEST: vale 41 | vpcConfig: # 私有网络配置 42 | vpcId: '' # 私有网络的Id 43 | subnetId: '' # 子网ID 44 | apigatewayConf: # api网关配置 45 | isDisabled: false # 是否禁用自动创建 API 网关功能 46 | isBase64Encoded: false # 是否开启 base64 编码 47 | enableCORS: true # 允许跨域 48 | customDomains: # 自定义域名绑定 49 | - domain: abc.com # 待绑定的自定义的域名 50 | certificateId: abcdefg # 待绑定自定义域名的证书唯一 ID 51 | # 如要设置自定义路径映射,请设置为 false 52 | isDefaultMapping: false 53 | # 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 54 | pathMappingSet: 55 | - path: / 56 | environment: release 57 | protocols: # 绑定自定义域名的协议类型,默认与服务的前端协议一致。 58 | - http # 支持http协议 59 | - https # 支持https协议 60 | protocols: 61 | - http 62 | - https 63 | environment: test 64 | serviceTimeout: 15 65 | usagePlan: # 用户使用计划 66 | usagePlanId: 1111 67 | usagePlanName: slscmp 68 | usagePlanDesc: sls create 69 | maxRequestNum: 1000 70 | auth: # 密钥 71 | secretName: secret 72 | secretIds: 73 | - xxx 74 | staticConf: 75 | cosConf: 76 | bucket: static-bucket 77 | sources: 78 | - src: .next/static 79 | targetDir: /_next/static 80 | - src: public 81 | targetDir: / 82 | cdnConf: 83 | area: mainland 84 | domain: cnode.yuga.chat 85 | autoRefresh: true 86 | refreshType: delete 87 | forceRedirect: 88 | switch: on 89 | redirectType: https 90 | redirectStatusCode: 301 91 | https: 92 | http2: on 93 | certId: 'abc' 94 | ``` 95 | 96 | ## 配置描述 97 | 98 | 主要的参数 99 | 100 | | 参数名称 | 必选 | 默认值 | 描述 | 101 | | ------------------------------------ | :--: | :-------------: | :-------------------------------------------------------------- | 102 | | runtime | 否 | `Nodejs10.15` | 执行环境, 支持: Nodejs6.10, Nodejs8.9, Nodejs10.15, Nodejs12.16 | 103 | | region | 否 | `ap-guangzhou` | 项目部署所在区域 | 104 | | functionName | 否 | | 云函数名称 | 105 | | serviceName | 否 | | API 网关服务名称, 默认创建一个新的服务名称 | 106 | | serviceId | 否 | | API 网关服务 ID, 如果存在将使用这个 API 网关服务 | 107 | | entryFile | 否 | `sls.js` | 自定义 server 的入口文件名 | 108 | | src | 否 | `process.cwd()` | 默认为当前目录, 如果是对象, 配置参数参考 [执行目录](#执行目录) | 109 | | layers | 否 | | 云函数绑定的 layer, 配置参数参考 [层配置](#层配置) | 110 | | [functionConf](#函数配置) | 否 | | 函数配置 | 111 | | [apigatewayConf](#API-网关配置) | 否 | | API 网关配置 | 112 | | [cloudDNSConf](#DNS-配置) | 否 | | DNS 配置 | 113 | | [Region special config](#指定区配置) | 否 | | 指定区配置 | 114 | | [staticConf](#静态资源-CDN-配置) | 否 | | 静态资源 CDN 配置 | 115 | 116 | ## 执行目录 117 | 118 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 119 | | -------- | :------: | :------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 120 | | src | 否 | String | | 代码路径。与 object 不能同时存在。 | 121 | | exclude | 否 | String[] | | 不包含的文件或路径, 遵守 [glob 语法](https://github.com/isaacs/node-glob) | 122 | | bucket | 否 | String | | bucket 名称。如果配置了 src,表示部署 src 的代码并压缩成 zip 后上传到 bucket-appid 对应的存储桶中;如果配置了 object,表示获取 bucket-appid 对应存储桶中 object 对应的代码进行部署。 | 123 | | object | 否 | String | | 部署的代码在存储桶中的路径。 | 124 | 125 | ## 层配置 126 | 127 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 128 | | -------- | :------: | :----: | :----: | :------- | 129 | | name | 否 | String | | 层名称 | 130 | | version | 否 | String | | 层版本号 | 131 | 132 | ### DNS 配置 133 | 134 | 参考: https://cloud.tencent.com/document/product/302/8516 135 | 136 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 137 | | ---------- | :------: | -------- | :----: | :---------------------------------------------- | 138 | | ttl | 否 | Number | `600` | TTL 值,范围 1 - 604800,不同等级域名最小值不同 | 139 | | recordLine | 否 | String[] | | 记录的线路名称 | 140 | 141 | ### 指定区配置 142 | 143 | | 参数名称 | 是否必选 | 类型 | 默认值 | 函数 | 144 | | ------------------------------- | :------: | ------ | ------ | ------------ | 145 | | [functionConf](#函数配置) | 否 | Object | | 函数配置 | 146 | | [apigatewayConf](#API-网关配置) | 否 | Object | | API 网关配置 | 147 | | [cloudDNSConf](#DNS-配置) | 否 | Object | | DNS 配置 | 148 | 149 | ### 函数配置 150 | 151 | 参考: https://cloud.tencent.com/document/product/583/18586 152 | 153 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 154 | | ----------- | :------: | :-----: | :-----: | :------------------------------------------------------------------------------ | 155 | | timeout | 否 | Number | `3` | 函数最长执行时间,单位为秒,可选值范围 1-900 秒,默认为 3 秒 | 156 | | memorySize | 否 | Number | `128` | 函数运行时内存大小,默认为 128M,可选范围 64、128MB-3072MB,并且以 128MB 为阶梯 | 157 | | environment | 否 | Object | | 函数的环境变量, 参考 [环境变量](#环境变量) | 158 | | vpcConfig | 否 | Object | | 函数的 VPC 配置, 参考 [VPC 配置](#VPC-配置) | 159 | | eip | 否 | Boolean | `false` | 是否固定出口 IP | 160 | 161 | ##### 环境变量 162 | 163 | | 参数名称 | 类型 | 描述 | 164 | | --------- | ---- | :---------------------------------------- | 165 | | variables | | 环境变量参数, 包含多对 key-value 的键值对 | 166 | 167 | ##### VPC 配置 168 | 169 | | 参数名称 | 类型 | 描述 | 170 | | -------- | ------ | :------ | 171 | | subnetId | String | 子网 ID | 172 | | vpcId | String | VPC ID | 173 | 174 | ### API 网关配置 175 | 176 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 177 | | --------------- | :------: | :------- | :--------- | :--------------------------------------------------------------------------------- | 178 | | protocols | 否 | String[] | `['http']` | 前端请求的类型,如 http,https,http 与 https | 179 | | environment | 否 | String | `release` | 发布环境. 目前支持三种发布环境: test(测试), prepub(预发布) 与 release(发布). | 180 | | usagePlan | 否 | | | 使用计划配置, 参考 [使用计划](#使用计划) | 181 | | auth | 否 | | | API 密钥配置, 参考 [API 密钥](#API-密钥配置) | 182 | | customDomain | 否 | Object[] | | 自定义 API 域名配置, 参考 [自定义域名](#自定义域名) | 183 | | enableCORS | 否 | Boolean | `false` | 开启跨域。默认值为否。 | 184 | | serviceTimeout | 否 | Number | `15` | Api 超时时间,单位: 秒 | 185 | | isDisabled | 否 | Boolean | `false` | 关闭自动创建 API 网关功能。默认值为否,即默认自动创建 API 网关。 | 186 | | isBase64Encoded | 否 | Boolean | `false` | 是否开启 Base64 编码,如果需要文件上传,请配置为 `true` | 187 | 188 | ##### 使用计划 189 | 190 | 参考: https://cloud.tencent.com/document/product/628/14947 191 | 192 | | 参数名称 | 是否必选 | 类型 | 描述 | 193 | | ------------- | :------: | ------ | :------------------------------------------------------ | 194 | | usagePlanId | 否 | String | 用户自定义使用计划 ID | 195 | | usagePlanName | 否 | String | 用户自定义的使用计划名称 | 196 | | usagePlanDesc | 否 | String | 用户自定义的使用计划描述 | 197 | | maxRequestNum | 否 | Number | 请求配额总数,如果为空,将使用-1 作为默认值,表示不开启 | 198 | 199 | ##### API 密钥配置 200 | 201 | 参考: https://cloud.tencent.com/document/product/628/14916 202 | 203 | | 参数名称 | 类型 | 描述 | 204 | | ---------- | :----- | :------- | 205 | | secretName | String | 密钥名称 | 206 | | secretIds | String | 密钥 ID | 207 | 208 | ##### 自定义域名 209 | 210 | Refer to: https://cloud.tencent.com/document/product/628/14906 211 | 212 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 213 | | ---------------- | :------: | :------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 214 | | domain | 是 | String | | 待绑定的自定义的域名。 | 215 | | certificateId | 否 | String | | 待绑定自定义域名的证书唯一 ID,如果设置了 type 为 https,则为必选 | 216 | | isDefaultMapping | 否 | String | `true` | 是否使用默认路径映射。为 false 时,表示自定义路径映射,此时 pathMappingSet 必填。 | 217 | | pathMappingSet | 否 | Object[] | `[]` | 自定义路径映射的路径。使用自定义映射时,可一次仅映射一个 path 到一个环境,也可映射多个 path 到多个环境。并且一旦使用自定义映射,原本的默认映射规则不再生效,只有自定义映射路径生效。 | 218 | | protocol | 否 | String[] | | 绑定自定义域名的协议类型,默认与服务的前端协议一致。 | 219 | 220 | - 自定义路径映射 221 | 222 | | 参数名称 | 是否必选 | 类型 | Description | 223 | | ----------- | :------: | :----- | :------------- | 224 | | path | 是 | String | 自定义映射路径 | 225 | | environment | 是 | String | 自定义映射环境 | 226 | 227 | ### 静态资源 CDN 配置 228 | 229 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 230 | | -------- | :------: | :----: | :----: | :-------------------- | 231 | | cosConf | 是 | Object | | [COS 配置](#cos-配置) | 232 | | cdnConf | 否 | Object | | [CDN 配置](#cdn-配置) | 233 | 234 | ##### COS 配置 235 | 236 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 237 | | -------- | :------: | :------: | :------------------------------------------------------------------------------------: | :----------------------------- | 238 | | bucket | 是 | string | | COS 存储同名称,没有将自动创建 | 239 | | (#acl) | 240 | | sources | 否 | Object[] | `[{src: '.next/static', targetDir: '/_next/static'}, {src: 'public', targetDir: '/'}]` | 需要托管到 COS 的静态资源目录 | 241 | 242 | ##### CDN 配置 243 | 244 | area: mainland domain: cnode.yuga.chat autoRefresh: true refreshType: delete 245 | forceRedirect: switch: on redirectType: https redirectStatusCode: 301 https: 246 | http2: on certId: 'eGkM75xv' 247 | 248 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 249 | | ------------- | :------: | :-----: | :--------: | :--------------------------------------------------------- | 250 | | domain | 是 | string | | CDN 域名 | 251 | | area | 否 | string | `mainland` | 加速区域,mainland: 大陆,overseas:海外,global:全球加速 | 252 | | autoRefresh | 否 | boolean | `true` | 是否自动刷新 CDN | 253 | | refreshType | 否 | boolean | `delete` | CDN 刷新类型,delete:刷新全部资源,flush:刷新变更资源 | 254 | | forceRedirect | 否 | Object | | 访问协议强制跳转配置,参考 [forceRedirect](#forceRedirect) | 255 | | https | 否 | Object | | https 配置,参考 [https](#https) | 256 | 257 | ###### forceRedirect 258 | 259 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 260 | | ------------------ | :------: | :----: | :----: | :------------------------------------------------------------- | 261 | | switch | 是 | string | `on` | 访问强制跳转配置开关, on:开启,off:关闭 | 262 | | redirectType | 是 | string | `http` | 访问强制跳转类型,http:强制 http 跳转,https:强制 https 跳转 | 263 | | redirectStatusCode | 是 | number | `301` | 强制跳转时返回状态码,支持 301、302 | 264 | 265 | ###### https 266 | 267 | | 参数名称 | 是否必选 | 类型 | 默认值 | 描述 | 268 | | -------- | :------: | :----: | :----: | :------------------------------------ | 269 | | certId | 是 | string | | 腾讯云托管域名证书 ID | 270 | | http2 | 是 | string | | 是否开启 HTTP2,on: 开启,off: 关闭 | 271 | --------------------------------------------------------------------------------