Skip to content

Commit

Permalink
feat: vercel starter and trigger (#1199)
Browse files Browse the repository at this point in the history
  • Loading branch information
echosoar authored Aug 6, 2021
1 parent eae854a commit 7d978a2
Show file tree
Hide file tree
Showing 18 changed files with 598 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages-serverless/serverless-app/src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ export const StarterMap = {
tencent: ['@midwayjs/serverless-scf-starter'],
gaia: ['@ali/serverless-gaia-starter'],
ginkgo: ['@ali/serverless-ginkgo-starter'],
vercel: ['@ali/serverless-vercel-starter'],
};

export const TriggerMap = {
aliyun: ['@midwayjs/serverless-fc-trigger'],
tencent: ['@midwayjs/serverless-scf-trigger'],
gaia: ['@ali/serverless-gaia-trigger'],
ginkgo: ['@ali/serverless-ginkgo-trigger'],
vercel: ['@ali/serverless-vercel-trigger'],
};
1 change: 1 addition & 0 deletions packages-serverless/serverless-vercel-starter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# serverless vercel starter
7 changes: 7 additions & 0 deletions packages-serverless/serverless-vercel-starter/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['./jest.setup.js'],
};
2 changes: 2 additions & 0 deletions packages-serverless/serverless-vercel-starter/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.env.MIDWAY_TS_MODE = 'true';
jest.setTimeout(30000);
35 changes: 35 additions & 0 deletions packages-serverless/serverless-vercel-starter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "serverless-vercel-starter",
"version": "2.12.1",
"main": "dist/index",
"typings": "dist/index.d.ts",
"dependencies": {
"@midwayjs/runtime-engine": "^2.11.2",
"@midwayjs/serverless-http-parser": "^2.11.4",
"parseurl": "^1.3.3"
},
"devDependencies": {
"@midwayjs/core": "^2.12.1",
"@midwayjs/decorator": "^2.11.5",
"@midwayjs/runtime-mock": "^2.11.2",
"raw-body": "^2.4.1"
},
"engines": {
"node": ">= 10"
},
"files": [
"dist/**/*.js",
"dist/**/*.d.ts"
],
"scripts": {
"build": "tsc",
"test": "node --require=ts-node/register ../../node_modules/.bin/jest",
"cov": "node --require=ts-node/register ../../node_modules/.bin/jest --coverage --forceExit"
},
"repository": {
"type": "git",
"url": "git@github.com:midwayjs/midway.git"
},
"license": "MIT",
"gitHead": "1afeb34cbb6c0ad49ccdb9cfaebd254ae73afc6a"
}
28 changes: 28 additions & 0 deletions packages-serverless/serverless-vercel-starter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseBootstrap } from '@midwayjs/runtime-engine';
import { VercelRuntime } from './runtime';

export { asyncWrapper } from '@midwayjs/runtime-engine';
export * from './runtime';

let bootstrap;

export const start = async (options: any = {}) => {
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = 'production';
}

bootstrap = new BaseBootstrap(
Object.assign(
{
runtime: new VercelRuntime(),
},
options
)
);
await bootstrap.start();
return bootstrap.getRuntime();
};

export const close = async () => {
return bootstrap.close();
};
134 changes: 134 additions & 0 deletions packages-serverless/serverless-vercel-starter/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as getRawBody from 'raw-body';
import { ServerlessLightRuntime } from '@midwayjs/runtime-engine';
import { Application, HTTPResponse } from '@midwayjs/serverless-http-parser';
import * as parseurl from 'parseurl';

const isOutputError = () => {
return (
process.env.SERVERLESS_OUTPUT_ERROR_STACK === 'true' ||
['local', 'development'].includes(process.env.MIDWAY_SERVER_ENV) ||
['local', 'development'].includes(process.env.NODE_ENV)
);
};

export class VercelRuntime extends ServerlessLightRuntime {
app;
respond;

init(contextExtensions) {
super.init(contextExtensions);
this.app = new Application();
}

/**
* for handler wrapper
* @param handler
*/
asyncEvent(handler) {
if (handler && handler.constructor.name !== 'AsyncFunction') {
throw new TypeError('Must be an AsyncFunction');
}

return (...args) => {
const [req, res] = args;
return this.wrapperWebInvoker(handler, req, res);
};
}

async wrapperWebInvoker(handler, req, res) {
if (!this.respond) {
this.respond = this.app.callback();
}

const newRes = new HTTPResponse();

req.getOriginContext = () => {
return {};
};

// vercel req is IncomingMessage , no path
const newUrlInfo = parseurl({ url: req.url });
req.path = newUrlInfo.pathname;

if (
['post', 'put', 'delete'].indexOf(req.method.toLowerCase()) !== -1 &&
!req.body &&
typeof req.on === 'function'
) {
req.body = await getRawBody(req); // TODO: body parser
}
const newReq = req;

return this.respond.apply(this.respond, [
newReq,
newRes,
ctx => {
return this.invokeHandlerWrapper(ctx, async () => {
if (!handler) {
const args = [ctx];
return this.defaultInvokeHandler(...args);
}
return handler.apply(handler, [ctx]);
})
.then(result => {
if (res.headersSent) {
return;
}

if (result) {
ctx.body = result;
}

if (!ctx.response._explicitStatus) {
if (ctx.body === null || ctx.body === undefined) {
ctx.body = '';
ctx.type = 'text';
ctx.status = 204;
}
}

const newHeader = {};
for (const key in ctx.res.headers) {
// The length after base64 is wrong.
if (!['content-length'].includes(key)) {
newHeader[key] = ctx.res.headers[key];
}
}

for (const key in newHeader) {
res.setHeader(key, newHeader[key]);
}

if (res.statusCode !== ctx.status) {
res.status(ctx.status);
}
// vercel support object/string/buffer
res.send(ctx.body);
})
.catch(err => {
ctx.logger.error(err);
res.status(500);
res.send(isOutputError() ? err.stack : 'Internal Server Error');
});
},
]);
}

async beforeInvokeHandler(context) {}

async afterInvokeHandler(err, result, context) {}

getApplication() {
return this.app;
}

getFunctionName(): string {
return this.options?.initContext?.function?.name || super.getFunctionName();
}

getFunctionServiceName(): string {
return (
this.options?.initContext?.service?.name || super.getFunctionServiceName()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { asyncWrapper, start, close } from '../../../src';

let runtime;
let inited;

exports.handler = asyncWrapper(async (...args) => {
if (!inited) {
inited = true;
runtime = await start();
}
const res = await runtime.asyncEvent(async function (ctx) {
const app = runtime.getApplication();
const functionName = runtime.getFunctionName();
const functionServiceName = runtime.getFunctionServiceName();
if (ctx.query?.str) {
return '123';
}
if (ctx.query?.noReturn) {
return;
}
if (ctx.query?.undefined) {
return undefined;
}
if (ctx.query?.error) {
throw new Error('error')
}
if (ctx.query?.buffer) {
ctx.status = 401;
return Buffer.from('123');
}
return { path: ctx.path, functionName, functionServiceName, isExistsApp: !!app };
})(...args);
await close();
return res;
});
104 changes: 104 additions & 0 deletions packages-serverless/serverless-vercel-starter/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { createRuntime } from '@midwayjs/runtime-mock';
import * as assert from 'assert';
// 这里不能用包引,会循环依赖
import { HTTPTrigger } from '../../serverless-vercel-trigger/src';
import { join } from 'path';

describe('/test/index.test.ts', () => {
it('should invoke normal code', async () => {
const runtime = createRuntime({
functionDir: join(__dirname, './fixtures/http'),
});
await runtime.start();
const trigger = new HTTPTrigger({
path: '/help',
method: 'GET',
});
const result = await runtime.invoke(trigger);
assert.equal(JSON.parse(result.body).path, '/help');
await runtime.close();
});
it('should get string response', async () => {
const runtime = createRuntime({
functionDir: join(__dirname, './fixtures/http'),
});
await runtime.start();
const trigger = new HTTPTrigger({
path: '/help',
method: 'GET',
query: {
str: true
}
});
const result = await runtime.invoke(trigger);
assert.equal(result.body, '123');
assert(result.headers['content-type'].includes('text/plain'));
await runtime.close();
});
it('should get buffer response', async () => {
const runtime = createRuntime({
functionDir: join(__dirname, './fixtures/http'),
});
await runtime.start();
const trigger = new HTTPTrigger({
path: '/help',
method: 'GET',
query: {
buffer: true
}
});
const result = await runtime.invoke(trigger);
assert.equal(result.body, '123');
assert.equal(result.statusCode, 401);
assert.equal(result.headers['content-type'], 'application/octet-stream');
await runtime.close();
});
it('should get empty response', async () => {
const runtime = createRuntime({
functionDir: join(__dirname, './fixtures/http'),
});
await runtime.start();
const trigger = new HTTPTrigger({
path: '/help',
method: 'GET',
query: {
noReturn: true
}
});
const result = await runtime.invoke(trigger);
assert.equal(result.statusCode, 204);
await runtime.close();
});
it('should get error', async () => {
const runtime = createRuntime({
functionDir: join(__dirname, './fixtures/http'),
});
await runtime.start();
const trigger = new HTTPTrigger({
path: '/help',
method: 'GET',
query: {
error: true
}
});
const result = await runtime.invoke(trigger);
assert.equal(result.statusCode, 500);
await runtime.close();
});
it('should get error', async () => {
const runtime = createRuntime({
functionDir: join(__dirname, './fixtures/http'),
});
await runtime.start();
const trigger = new HTTPTrigger({
path: '/help',
method: 'GET',
query: {
undefined: true
}
});
const result = await runtime.invoke(trigger);
assert.equal(result.statusCode, 204);
await runtime.close();
});
});
11 changes: 11 additions & 0 deletions packages-serverless/serverless-vercel-starter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compileOnSave": true,
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": [
"./src/**/*.ts"
]
}
7 changes: 7 additions & 0 deletions packages-serverless/serverless-vercel-trigger/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['./jest.setup.js'],
};
2 changes: 2 additions & 0 deletions packages-serverless/serverless-vercel-trigger/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.env.MIDWAY_TS_MODE = 'true';
jest.setTimeout(30000);
Loading

0 comments on commit 7d978a2

Please sign in to comment.