├── .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 |
--------------------------------------------------------------------------------
/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 | [](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 |
--------------------------------------------------------------------------------