diff --git a/packages/faas-cli-command-core/src/core.ts b/packages/faas-cli-command-core/src/core.ts index 0cab5fe2d1f7..81cae8b9860f 100644 --- a/packages/faas-cli-command-core/src/core.ts +++ b/packages/faas-cli-command-core/src/core.ts @@ -29,6 +29,7 @@ export class CommandHookCore implements ICommandHooksCore { private execId: number = Math.ceil(Math.random() * 1000); private userLifecycle: any = {}; private cwd: string; + private stopLifecycles: string[] = []; store = new Map(); @@ -138,6 +139,10 @@ export class CommandHookCore implements ICommandHooksCore { if (displayHelp) { return this.displayHelp(commandsArray, commandInfo.usage); } + await this.execLiftcycle(lifecycleEvents); + } + + private async execLiftcycle(lifecycleEvents) { for (const lifecycle of lifecycleEvents) { if (this.userLifecycle && this.userLifecycle[lifecycle]) { this.debug('User Lifecycle', lifecycle); @@ -161,6 +166,17 @@ export class CommandHookCore implements ICommandHooksCore { } } + // resume stop licycle execute + public async resume(options?) { + if (options) { + if (!this.options.options) { + this.options.options = {}; + } + Object.assign(this.options.options, options); + } + await this.execLiftcycle(this.stopLifecycles); + } + // spawn('aliyun:invoke') public async spawn(commandsArray: string | string[], options?: any) { let commands: string[] = []; @@ -314,6 +330,7 @@ export class CommandHookCore implements ICommandHooksCore { parentCommandList?: string[] ) { const allLifecycles: string[] = []; + let isStop = false; const { stopLifecycle } = this.options; const parentCommand = parentCommandList && parentCommandList.length @@ -321,12 +338,13 @@ export class CommandHookCore implements ICommandHooksCore { : ''; if (lifecycleEvents) { for (const life of lifecycleEvents) { + const liftCycles = isStop ? this.stopLifecycles : allLifecycles; const tmpLife = `${parentCommand}${command}:${life}`; - allLifecycles.push(`before:${tmpLife}`); - allLifecycles.push(tmpLife); - allLifecycles.push(`after:${tmpLife}`); + liftCycles.push(`before:${tmpLife}`); + liftCycles.push(tmpLife); + liftCycles.push(`after:${tmpLife}`); if (stopLifecycle === tmpLife) { - return allLifecycles; + isStop = true; } } } diff --git a/packages/faas-cli-command-core/test/core.test.ts b/packages/faas-cli-command-core/test/core.test.ts index 3383090dd5cc..9ab33825e84e 100644 --- a/packages/faas-cli-command-core/test/core.test.ts +++ b/packages/faas-cli-command-core/test/core.test.ts @@ -21,6 +21,24 @@ describe('command-core', () => { await core.invoke(['invoke']); assert(result && result.length === 3); }); + it('stop lifecycle and resume', async () => { + const result: string[] = []; + const core = new CommandHookCore({ + provider: 'test', + log: { + log: (msg: string) => { + result.push(msg); + }, + }, + stopLifecycle: 'invoke:one', + }); + core.addPlugin(TestPlugin); + await core.ready(); + await core.invoke(['invoke']); + assert(result.length === 3); + await core.resume(); + assert((result as any).length === 6); + }); it('user lifecycle', async () => { const cwd = join(__dirname, './fixtures/userLifecycle'); const tmpData = Date.now() + ''; diff --git a/packages/faas-cli-command-core/test/fixtures/userLifecycle/test.txt b/packages/faas-cli-command-core/test/fixtures/userLifecycle/test.txt index 3fee2d73affc..59c24a42d2f9 100644 --- a/packages/faas-cli-command-core/test/fixtures/userLifecycle/test.txt +++ b/packages/faas-cli-command-core/test/fixtures/userLifecycle/test.txt @@ -1 +1 @@ -1595514742831 \ No newline at end of file +1595516163422 diff --git a/packages/faas-cli-plugin-invoke/package.json b/packages/faas-cli-plugin-invoke/package.json index 28c15da72201..c98edbe80640 100644 --- a/packages/faas-cli-plugin-invoke/package.json +++ b/packages/faas-cli-plugin-invoke/package.json @@ -4,7 +4,7 @@ "main": "dist/index", "typings": "dist/index.d.ts", "dependencies": { - "@midwayjs/debugger": "^0.0.3", + "@midwayjs/debugger": "^1.0.0", "@midwayjs/faas-code-analysis": "^1.1.0", "@midwayjs/fcli-command-core": "^1.1.2", "@midwayjs/locate": "^1.0.3", diff --git a/packages/faas-cli-plugin-invoke/src/index.ts b/packages/faas-cli-plugin-invoke/src/index.ts index 53380277397d..e5b6172c21a2 100644 --- a/packages/faas-cli-plugin-invoke/src/index.ts +++ b/packages/faas-cli-plugin-invoke/src/index.ts @@ -1,18 +1,31 @@ import { BasePlugin } from '@midwayjs/fcli-command-core'; import { AnalyzeResult, Locator } from '@midwayjs/locate'; import { - analysis, + analysisResultToSpec, compareFileChange, copyFiles, } from '@midwayjs/faas-code-analysis'; -import { CompilerHost, Program, resolveTsConfigFile } from '@midwayjs/mwcc'; +import { + CompilerHost, + Program, + resolveTsConfigFile, + Analyzer, +} from '@midwayjs/mwcc'; import { writeWrapper } from '@midwayjs/serverless-spec-builder'; import { createRuntime } from '@midwayjs/runtime-mock'; import * as FCTrigger from '@midwayjs/serverless-fc-trigger'; import * as SCFTrigger from '@midwayjs/serverless-scf-trigger'; import { resolve, relative, join } from 'path'; -import { FaaSStarterClass, cleanTarget } from './utils'; -import { type } from 'os'; +import { + FaaSStarterClass, + checkIsTsMode, + cleanTarget, + getLock, + getPlatformPath, + setLock, + waitForLock, + LOCK_TYPE, +} from './utils'; import { ensureFileSync, existsSync, @@ -25,12 +38,8 @@ import { mkdirSync, } from 'fs-extra'; export * from './invoke'; -const commonLock: any = {}; -enum LOCK_TYPE { - INITIAL, - WAITING, - COMPLETE, -} +export * from './interface'; +export * from './utils'; export class FaaSInvokePlugin extends BasePlugin { baseDir: string; @@ -119,60 +128,25 @@ export class FaaSInvokePlugin extends BasePlugin { this.setStore('defaultTmpFaaSOut', this.defaultTmpFaaSOut); } - getLock(lockKey) { - if (!commonLock[lockKey]) { - commonLock[lockKey] = { - lockType: LOCK_TYPE.INITIAL, - lockData: {}, - }; - } - return commonLock[lockKey]; - } - - setLock(lockKey, status, data?) { - if (!commonLock[lockKey]) { - return; - } - commonLock[lockKey].lockType = status; - commonLock[lockKey].lockData = data; - } - - async waitForLock(lockKey, count?) { - count = count || 0; - return new Promise(resolve => { - if (count > 100) { - return resolve(); - } - const { lockType, lockData } = this.getLock(lockKey); - if (lockType === LOCK_TYPE.WAITING) { - setTimeout(() => { - this.waitForLock(lockKey, count + 1).then(resolve); - }, 300); - } else { - resolve(lockData); - } - }); - } - async locator() { this.baseDir = this.core.config.servicePath; this.buildDir = resolve(this.baseDir, '.faas_debug_tmp'); const lockKey = `codeAnalyzeResult:${this.baseDir}`; - const { lockType, lockData } = this.getLock(lockKey); + const { lockType, lockData } = getLock(lockKey); let codeAnalyzeResult; if (lockType === LOCK_TYPE.INITIAL) { - this.setLock(lockKey, LOCK_TYPE.WAITING); + setLock(lockKey, LOCK_TYPE.WAITING); // 分析目录结构 const locator = new Locator(this.baseDir); codeAnalyzeResult = await locator.run({ tsCodeRoot: this.options.sourceDir, tsBuildRoot: this.buildDir, }); - this.setLock(lockKey, LOCK_TYPE.COMPLETE, codeAnalyzeResult); + setLock(lockKey, LOCK_TYPE.COMPLETE, codeAnalyzeResult); } else if (lockType === LOCK_TYPE.COMPLETE) { codeAnalyzeResult = lockData; } else if (lockType === LOCK_TYPE.WAITING) { - codeAnalyzeResult = await this.waitForLock(lockKey); + codeAnalyzeResult = await waitForLock(lockKey); } this.codeAnalyzeResult = codeAnalyzeResult; } @@ -194,6 +168,15 @@ export class FaaSInvokePlugin extends BasePlugin { }); } + public getTsCodeRoot() { + const tmpOutDir = resolve(this.defaultTmpFaaSOut, 'src'); + if (existsSync(tmpOutDir)) { + return tmpOutDir; + } else { + return this.codeAnalyzeResult.tsCodeRoot; + } + } + async checkFileChange() { const tsconfig = resolve(this.baseDir, 'tsconfig.json'); // 非ts @@ -202,7 +185,7 @@ export class FaaSInvokePlugin extends BasePlugin { return; } this.skipTsBuild = false; - const isTsMode = this.checkIsTsMode(); + const isTsMode = checkIsTsMode(); if (!isTsMode) { process.env.MIDWAY_TS_MODE = 'false'; } @@ -215,26 +198,24 @@ export class FaaSInvokePlugin extends BasePlugin { )); this.analysisCodeInfoPath = resolve(this.buildLogDir, '.faasFuncList.log'); - let directoryToScan: string; - const tmpOutDir = resolve(this.defaultTmpFaaSOut, 'src'); - if (existsSync(tmpOutDir)) { - this.analyzedTsCodeRoot = tmpOutDir; - directoryToScan = tmpOutDir; - } else { - this.analyzedTsCodeRoot = this.codeAnalyzeResult.tsCodeRoot; - directoryToScan = relative(this.baseDir, this.analyzedTsCodeRoot); - } + // 获取要分析的代码目录 + this.analyzedTsCodeRoot = this.getTsCodeRoot(); + // 扫描文件查看是否发生变化,乳沟没有变化就跳过编译 + const directoryToScan: string = relative( + this.baseDir, + this.analyzedTsCodeRoot + ); if (isTsMode) { return; } - const { lockType } = this.getLock(this.buildLockPath); + const { lockType } = getLock(this.buildLockPath); this.core.debug('lockType', lockType); // 如果当前存在构建任务,那么就进行等待 if (lockType === LOCK_TYPE.INITIAL) { - this.setLock(this.buildLockPath, LOCK_TYPE.WAITING); + setLock(this.buildLockPath, LOCK_TYPE.WAITING); } else if (lockType === LOCK_TYPE.WAITING) { - await this.waitForLock(this.buildLockPath); + await waitForLock(this.buildLockPath); } const specFile = this.core.config.specFile.path; @@ -248,7 +229,7 @@ export class FaaSInvokePlugin extends BasePlugin { ); if (!this.fileChanges || !this.fileChanges.length) { this.getAnaLysisCodeInfo(); - this.setLock(this.buildLockPath, LOCK_TYPE.COMPLETE); + setLock(this.buildLockPath, LOCK_TYPE.COMPLETE); this.skipTsBuild = true; this.setStore('skipTsBuild', true); this.core.debug('Auto skip ts compile'); @@ -262,7 +243,7 @@ export class FaaSInvokePlugin extends BasePlugin { // } } this.core.debug('fileChanges', this.fileChanges); - this.setLock(this.buildLockPath, LOCK_TYPE.WAITING); + setLock(this.buildLockPath, LOCK_TYPE.WAITING); } async compile() { @@ -293,10 +274,12 @@ export class FaaSInvokePlugin extends BasePlugin { // 当spec上面没有functions的时候,启动代码分析 if (!this.core.service.functions) { - const newSpec = await analysis([ - resolve(this.baseDir, this.analyzedTsCodeRoot), - resolve(this.defaultTmpFaaSOut, 'src'), - ]); + const analyzeInstance = new Analyzer({ + program: this.program, + decoratorLowerCase: true, + }); + const analyzeResult = analyzeInstance.analyze(); + const newSpec = await analysisResultToSpec(analyzeResult); this.core.debug('Code Analysis Result', newSpec); this.core.service.functions = newSpec.functions; this.setStore('functions', this.core.service.functions); @@ -307,7 +290,7 @@ export class FaaSInvokePlugin extends BasePlugin { } if (this.core.pluginManager.options.stopLifecycle === 'invoke:compile') { // LOCK_TYPE.INITIAL 是因为跳过了ts编译,下一次来的时候还是得进行ts编译 - this.setLock(this.buildLockPath, LOCK_TYPE.INITIAL); + setLock(this.buildLockPath, LOCK_TYPE.INITIAL); } return this.core.service.functions; } @@ -326,7 +309,7 @@ export class FaaSInvokePlugin extends BasePlugin { } async emit() { - const isTsMode = this.checkIsTsMode(); + const isTsMode = checkIsTsMode(); if (isTsMode || this.skipTsBuild) { return; } @@ -341,11 +324,11 @@ export class FaaSInvokePlugin extends BasePlugin { } } catch (e) { await remove(this.buildLockPath); - this.setLock(this.buildLockPath, LOCK_TYPE.COMPLETE); + setLock(this.buildLockPath, LOCK_TYPE.COMPLETE); this.core.debug('Typescript Build Error', e); throw new Error('Typescript Build Error, Please Check Your FaaS Code!'); } - this.setLock(this.buildLockPath, LOCK_TYPE.COMPLETE); + setLock(this.buildLockPath, LOCK_TYPE.COMPLETE); // 针对多次调用清理缓存 Object.keys(require.cache).forEach(path => { if (path.indexOf(this.buildDir) !== -1) { @@ -376,23 +359,38 @@ export class FaaSInvokePlugin extends BasePlugin { async entry() { const { name, fileName, userEntry } = this.checkUserEntry(); if (!userEntry) { - const isTsMode = this.checkIsTsMode(); + const isTsMode = checkIsTsMode(); const starterName = this.getStarterName(); if (!starterName) { return; } + const { + faasModName, + initializeName, + faasStarterName, + advancePreventMultiInit, + } = this.getEntryInfo(); + + // 获取中间件 + const mw = this.core.service['feature'] || {}; + const middleware = Object.keys(mw).filter(item => !!mw[item]); + writeWrapper({ baseDir: this.baseDir, + middleware, + faasModName, + initializeName, + faasStarterName, + advancePreventMultiInit, service: { layers: this.core.service.layers, functions: this.core.service.functions, }, - faasModName: process.env.MidwayModuleName, distDir: this.buildDir, - starter: this.getPlatformPath(starterName), + starter: getPlatformPath(starterName), loadDirectory: isTsMode - ? [this.getPlatformPath(resolve(this.defaultTmpFaaSOut, 'src'))] + ? [getPlatformPath(resolve(this.defaultTmpFaaSOut, 'src'))] : [], }); if (isTsMode) { @@ -414,6 +412,15 @@ export class FaaSInvokePlugin extends BasePlugin { this.core.debug('EntryInfo', this.entryInfo); } + public getEntryInfo() { + return { + faasModName: process.env.MidwayModuleName, + initializeName: 'initializer', + faasStarterName: 'FaaSStarter', + advancePreventMultiInit: false, + }; + } + getStarterName() { const platform = this.getPlatform(); this.core.debug('Platform entry', platform); @@ -593,15 +600,7 @@ export class FaaSInvokePlugin extends BasePlugin { return starter; } - checkIsTsMode(): boolean { - // eslint-disable-next-line node/no-deprecated-api - return process.env.MIDWAY_TS_MODE === 'true' && !!require.extensions['.ts']; - } - - getPlatformPath(p) { - if (type() === 'Windows_NT') { - return p.replace(/\\/g, '\\\\'); - } - return p; + checkIsTsMode() { + return checkIsTsMode(); } } diff --git a/packages/faas-cli-plugin-invoke/src/interface.ts b/packages/faas-cli-plugin-invoke/src/interface.ts new file mode 100644 index 000000000000..8443991775ed --- /dev/null +++ b/packages/faas-cli-plugin-invoke/src/interface.ts @@ -0,0 +1,12 @@ +export interface InvokeOptions { + functionDir?: string; // 函数所在目录 + functionName?: string; // 函数名 + data?: any[]; // 函数入参 + trigger?: string; // 触发器 + handler?: string; + sourceDir?: string; // 一体化目录结构下,函数的目录,比如 src/apis,这个影响到编译 + clean?: boolean; // 清理调试目录 + incremental?: boolean; // 增量编译 + verbose?: boolean | string; // 输出更多信息 + getFunctionList?: boolean; // 获取函数列表 +} diff --git a/packages/faas-cli-plugin-invoke/src/invoke.ts b/packages/faas-cli-plugin-invoke/src/invoke.ts index bc5c3c76bf95..4e41e1376209 100644 --- a/packages/faas-cli-plugin-invoke/src/invoke.ts +++ b/packages/faas-cli-plugin-invoke/src/invoke.ts @@ -4,21 +4,11 @@ import { getSpecFile, } from '@midwayjs/fcli-command-core'; import { FaaSInvokePlugin } from './index'; +import { formatInvokeResult, optionsToInvokeParams } from './utils'; +import { InvokeOptions } from './interface'; const { debugWrapper } = require('@midwayjs/debugger'); -export interface InvokeOptions { - functionDir?: string; // 函数所在目录 - functionName: string; // 函数名 - data?: any[]; // 函数入参 - trigger?: string; // 触发器 - handler?: string; - sourceDir?: string; // 一体化目录结构下,函数的目录,比如 src/apis,这个影响到编译 - clean?: boolean; // 清理调试目录 - incremental?: boolean; // 增量编译 - verbose?: boolean | string; // 输出更多信息 -} - -export const getFunction = getOptions => { +export const getFunction = (getOptions: any = {}) => { return async (options: any) => { const baseDir = options.functionDir || process.cwd(); const specFile = getOptions.specFile || getSpecFile(baseDir); @@ -30,37 +20,42 @@ export const getFunction = getOptions => { commands: ['invoke'], service: getOptions.spec || loadSpec(baseDir, specFile), provider: '', - options: { - function: options.functionName, - data: options.data, - trigger: options.trigger, - handler: options.handler, - sourceDir: options.sourceDir, - clean: options.clean, - incremental: options.incremental, - verbose: options.verbose, - resultType: 'store', - }, + options: optionsToInvokeParams(options), log: console, stopLifecycle: getOptions.stopLifecycle, }); core.addPlugin(FaaSInvokePlugin); await core.ready(); await core.invoke(['invoke']); - return core.store.get('FaaSInvokePlugin:' + getOptions.key); + + return { + core, + getResult: key => { + return core.store.get('FaaSInvokePlugin:' + key); + }, + }; }; }; export async function invokeFun(options: InvokeOptions) { const invokeFun = getFunction({ - key: 'result', + stopLifecycle: options.getFunctionList ? 'invoke:compile' : undefined, }); - const result = await invokeFun(options); - if (result.success) { - return result.result; - } else { - throw result.err; + const { core, getResult } = await invokeFun(options); + + if (!options.getFunctionList) { + const result = getResult('result'); + return formatInvokeResult(result); } + + return { + functionList: getResult('functions'), + invoke: async (options: InvokeOptions) => { + await core.resume(optionsToInvokeParams(options)); + const result = getResult('result'); + return formatInvokeResult(result); + }, + }; } export async function invoke(options: InvokeOptions) { @@ -88,11 +83,11 @@ export async function getFuncList(options: IGetFuncList) { } const invokeFun = getFunction({ stopLifecycle: 'invoke:compile', - key: 'functions', specFile, spec, }); options.clean = false; options.incremental = true; - return invokeFun(options); + const { getResult } = await invokeFun(options); + return getResult('functions'); } diff --git a/packages/faas-cli-plugin-invoke/src/utils.ts b/packages/faas-cli-plugin-invoke/src/utils.ts index ce45b19f2553..8df2b22ade03 100644 --- a/packages/faas-cli-plugin-invoke/src/utils.ts +++ b/packages/faas-cli-plugin-invoke/src/utils.ts @@ -1,5 +1,7 @@ import { existsSync, remove } from 'fs-extra'; import { join } from 'path'; +import { type } from 'os'; +import { InvokeOptions } from './interface'; export const exportMidwayFaaS = (() => { const midwayModuleName = process.env.MidwayModuleName || '@midwayjs/faas'; const faasPath = join(process.cwd(), './node_modules/', midwayModuleName); @@ -16,8 +18,87 @@ export const exportMidwayFaaS = (() => { export const FaaSStarterClass = exportMidwayFaaS.FaaSStarter; +// 清理某个目标目录 export const cleanTarget = async (p: string) => { if (existsSync(p)) { await remove(p); } }; + +// 格式化调用的返回值结果 +export const formatInvokeResult = result => { + if (result.success) { + return result.result; + } else { + throw result.err; + } +}; + +// 转换传递给 invoke 方法的参数 到 invoke plugin 所需要的参数 +export const optionsToInvokeParams = (options: InvokeOptions) => { + return { + function: options.functionName, + data: options.data, + trigger: options.trigger, + handler: options.handler, + sourceDir: options.sourceDir, + clean: options.clean, + incremental: options.incremental, + verbose: options.verbose, + resultType: 'store', + }; +}; + +const commonLock: any = {}; +export enum LOCK_TYPE { + INITIAL, + WAITING, + COMPLETE, +} + +export const getLock = lockKey => { + if (!commonLock[lockKey]) { + commonLock[lockKey] = { + lockType: LOCK_TYPE.INITIAL, + lockData: {}, + }; + } + return commonLock[lockKey]; +}; + +export const setLock = (lockKey, status, data?) => { + if (!commonLock[lockKey]) { + return; + } + commonLock[lockKey].lockType = status; + commonLock[lockKey].lockData = data; +}; + +export const waitForLock = async (lockKey, count?) => { + count = count || 0; + return new Promise(resolve => { + if (count > 100) { + return resolve(); + } + const { lockType, lockData } = getLock(lockKey); + if (lockType === LOCK_TYPE.WAITING) { + setTimeout(() => { + waitForLock(lockKey, count + 1).then(resolve); + }, 300); + } else { + resolve(lockData); + } + }); +}; + +export const checkIsTsMode = () => { + // eslint-disable-next-line node/no-deprecated-api + return process.env.MIDWAY_TS_MODE === 'true' && !!require.extensions['.ts']; +}; + +export const getPlatformPath = p => { + if (type() === 'Windows_NT') { + return p.replace(/\\/g, '\\\\'); + } + return p; +}; diff --git a/packages/faas-cli-plugin-invoke/test/index.test.ts b/packages/faas-cli-plugin-invoke/test/index.test.ts index 5703154f9f93..2e102cdbdd9b 100644 --- a/packages/faas-cli-plugin-invoke/test/index.test.ts +++ b/packages/faas-cli-plugin-invoke/test/index.test.ts @@ -47,6 +47,30 @@ describe('/test/index.test.ts', () => { await remove(join(__dirname, 'fixtures/baseApp/.faas_debug_tmp')); }); + it('invoke use two step', async () => { + process.env.MIDWAY_TS_MODE = 'true'; + const invokeInstance: any = await invoke({ + getFunctionList: true, + functionDir: join(__dirname, 'fixtures/baseApp'), + clean: false, + }); + assert(invokeInstance.functionList.http.handler === 'http.handler'); + assert(invokeInstance.invoke); + const result = await invokeInstance.invoke({ + functionName: 'http', + data: [{ name: 'params' }], + }); + process.env.MIDWAY_TS_MODE = 'false'; + assert(existsSync(join(__dirname, 'fixtures/baseApp/.faas_debug_tmp'))); + assert( + existsSync( + join(__dirname, 'fixtures/baseApp/.faas_debug_tmp/src/index.ts') + ) + ); + assert(result && result.body === 'hello http world'); + await remove(join(__dirname, 'fixtures/baseApp/.faas_debug_tmp')); + }); + it('should use origin http trigger in ice + faas demo by package options', async () => { const result: any = await invoke({ functionDir: join(__dirname, 'fixtures/ice-faas-ts-pkg-options'), diff --git a/packages/faas-cli-plugin-package/src/index.ts b/packages/faas-cli-plugin-package/src/index.ts index d5d5751c73fc..84be8485225a 100644 --- a/packages/faas-cli-plugin-package/src/index.ts +++ b/packages/faas-cli-plugin-package/src/index.ts @@ -19,8 +19,13 @@ import { } from 'fs-extra'; import * as micromatch from 'micromatch'; import { commonPrefix, formatLayers } from './utils'; -import { analysis, copyFiles } from '@midwayjs/faas-code-analysis'; -import { CompilerHost, Program, resolveTsConfigFile } from '@midwayjs/mwcc'; +import { analysisResultToSpec, copyFiles } from '@midwayjs/faas-code-analysis'; +import { + CompilerHost, + Program, + resolveTsConfigFile, + Analyzer, +} from '@midwayjs/mwcc'; import { exec } from 'child_process'; import * as archiver from 'archiver'; import { AnalyzeResult, Locator } from '@midwayjs/locate'; @@ -343,10 +348,13 @@ export class PackagePlugin extends BasePlugin { if (this.core.service.functions) { return this.core.service.functions; } - const newSpec: any = await analysis([ - resolve(this.servicePath, this.codeAnalyzeResult.tsCodeRoot), - resolve(this.defaultTmpFaaSOut, 'src'), - ]); + + const analyzeInstance = new Analyzer({ + program: this.program, + decoratorLowerCase: true, + }); + const analyzeResult = analyzeInstance.analyze(); + const newSpec = analysisResultToSpec(analyzeResult); this.core.debug('CodeAnalysis', newSpec); this.core.service.functions = newSpec.functions; } diff --git a/packages/faas-cli/package.json b/packages/faas-cli/package.json index 31e3a1e32b5d..6e191e445b64 100644 --- a/packages/faas-cli/package.json +++ b/packages/faas-cli/package.json @@ -4,7 +4,7 @@ "main": "dist/index", "typings": "dist/index.d.ts", "dependencies": { - "@midwayjs/debugger": "^0.0.3", + "@midwayjs/debugger": "^1.0.0", "@midwayjs/fcli-command-core": "^1.1.2", "@midwayjs/fcli-plugin-create": "^1.1.2", "@midwayjs/fcli-plugin-deploy": "^1.1.2", diff --git a/packages/faas-code-analysis/package.json b/packages/faas-code-analysis/package.json index 7f9625628e92..1b8b62a030a0 100644 --- a/packages/faas-code-analysis/package.json +++ b/packages/faas-code-analysis/package.json @@ -4,7 +4,7 @@ "main": "dist/index", "typings": "dist/index.d.ts", "dependencies": { - "@midwayjs/ts-analysis": "^0.1.5", + "@midwayjs/mwcc": "^0.3.2", "fs-extra": "^8.1.0", "globby": "^10.0.1" }, diff --git a/packages/faas-code-analysis/src/index.ts b/packages/faas-code-analysis/src/index.ts index f7dc62b65b1d..4228437d98b0 100644 --- a/packages/faas-code-analysis/src/index.ts +++ b/packages/faas-code-analysis/src/index.ts @@ -1,80 +1,105 @@ -import { tsAnalysisInstance, ITsAnalysisResult } from '@midwayjs/ts-analysis'; +import { Analyzer, AnalyzeResult } from '@midwayjs/mwcc'; import { formatUpperCamel, firstCharLower, getEventKey } from './utils'; import { IParam, IResult, IFunction, IEvent } from './interface'; export * from './interface'; export * from './utils'; export const analysis = async (codePath: IParam) => { + if (Array.isArray(codePath)) { + codePath = codePath[0]; + console.log('[warn] code analysi only support 1 source dir'); + } + const analysisInstance = new Analyzer({ + projectDir: codePath, + decoratorLowerCase: true, + }); + const analysisResult: AnalyzeResult = analysisInstance.analyze(); + return analysisResultToSpec(analysisResult); +}; + +export const analysisResultToSpec = (analysisResult: AnalyzeResult) => { const result: IResult = { functions: {}, }; - const analysisResult: ITsAnalysisResult = await tsAnalysisInstance(codePath, { - decoratorLowerCase: true, + + const provideList = analysisResult?.decorator?.provide || []; + provideList.forEach(provide => { + if (!provide.childDecorators.func) { + return; + } + provide.childDecorators.func.forEach(item => { + formatFuncInfo(result, item, provide.target); + }); }); - const funcList = analysisResult.decorator.func || []; - if (!funcList.length) { - return result; - } - funcList.forEach(item => { - const params = item.params; - let className = item.parent?.provide?.[0]?.target?.name || ''; - let funcName = item.target.name || 'handler'; + const funcList = analysisResult?.decorator?.func || []; - if (item.target.type === 'class') { - className = item.target.name; - funcName = 'handler'; - } - let handler; - let trigger: IEvent; - if (typeof params[0] === 'string') { - handler = params[0]; - trigger = params[1]; - } else { - handler = `${formatUpperCamel(className)}.${formatUpperCamel(funcName)}`; - trigger = params[0]; + funcList.forEach(item => { + if (item.target.type !== 'class') { + return; } + formatFuncInfo(result, item); + }); - const funName = handler.replace(/\.handler$/, '').replace(/\./g, '-'); + return result; +}; - const existsFuncData: IFunction = result.functions[funName] || { - handler: '', - events: [], +const formatFuncInfo = (result, funcInfo, parentTarget?) => { + const params = funcInfo.params; + let className = parentTarget?.name || ''; + let funcName = funcInfo.target.name || 'handler'; + + if (funcInfo.target.type === 'class') { + className = funcInfo.target.name; + funcName = 'handler'; + } + let handler; + let trigger: IEvent; + if (typeof params[0] === 'string') { + handler = params[0]; + trigger = params[1]; + } else { + handler = `${formatUpperCamel(className)}.${formatUpperCamel(funcName)}`; + trigger = params[0]; + } + + const funName = handler.replace(/\.handler$/, '').replace(/\./g, '-'); + + const existsFuncData: IFunction = result.functions[funName] || { + handler: '', + events: [], + }; + existsFuncData.handler = handler; + const events = existsFuncData.events || []; + + if (!trigger) { + trigger = { + event: 'http', }; - existsFuncData.handler = handler; - const events = existsFuncData.events || []; + } - if (!trigger) { - trigger = { - event: 'http', + if (trigger.event) { + const eventType = trigger.event.toLowerCase(); + const event: IEvent = { [eventType]: true }; + if (eventType === 'http') { + event.http = { + method: [(trigger.method || 'GET').toUpperCase()], + path: + trigger.path || + `/${firstCharLower(className)}/${firstCharLower(funcName)}`, }; } - - if (trigger.event) { - const eventType = trigger.event.toLowerCase(); - const event: IEvent = { [eventType]: true }; - if (eventType === 'http') { - event.http = { - method: [(trigger.method || 'GET').toUpperCase()], - path: - trigger.path || - `/${firstCharLower(className)}/${firstCharLower(funcName)}`, - }; - } - // 防止有重复的触发器 - const currentEventKey = getEventKey(eventType, event[eventType]); - const isExists = events.find(event => { - if (event[eventType]) { - const key = getEventKey(eventType, event[eventType]); - return key === currentEventKey; - } - }); - if (!isExists) { - events.push(event); + // 防止有重复的触发器 + const currentEventKey = getEventKey(eventType, event[eventType]); + const isExists = events.find(event => { + if (event[eventType]) { + const key = getEventKey(eventType, event[eventType]); + return key === currentEventKey; } + }); + if (!isExists) { + events.push(event); } - existsFuncData.events = events; - result.functions[funName] = existsFuncData; - }); - - return result; + } + existsFuncData.events = events; + result.functions[funName] = existsFuncData; }; diff --git a/packages/gateway-common-http/package.json b/packages/gateway-common-http/package.json index 863243e329b4..b0832589dc74 100644 --- a/packages/gateway-common-http/package.json +++ b/packages/gateway-common-http/package.json @@ -5,7 +5,6 @@ "typings": "dist/index.d.ts", "dependencies": { "@midwayjs/gateway-common-core": "^1.1.2", - "@midwayjs/serverless-invoke": "^1.1.2", "@midwayjs/serverless-spec-builder": "^1.1.2", "@types/express": "^4.17.0", "picomatch": "^2.2.1" diff --git a/packages/gateway-common-http/src/common.ts b/packages/gateway-common-http/src/common.ts index 93eb53a8448e..84cb7724d69a 100644 --- a/packages/gateway-common-http/src/common.ts +++ b/packages/gateway-common-http/src/common.ts @@ -1,25 +1,32 @@ import { DevPackOptions, InvokeOptions } from '@midwayjs/gateway-common-core'; import { isMatch } from 'picomatch'; import * as qs from 'querystring'; -import { getFuncList } from '@midwayjs/serverless-invoke'; const ignoreWildcardFunctionsWhiteList = []; export async function parseInvokeOptionsByOriginUrl( options: DevPackOptions, - req -): Promise> { + req, + getFuncList +): Promise<{ + invokeOptions: Partial; + invokeFun?: any; +}> { const ignorePattern = options.ignorePattern; const currentUrl = req.path || req.url; const currentMethod = req.method.toLowerCase(); if (ignorePattern) { if (typeof ignorePattern === 'function') { if (ignorePattern(req)) { - return {}; + return { + invokeOptions: {}, + }; } } else if (ignorePattern.length) { for (const pattern of ignorePattern as string[]) { if (new RegExp(pattern).test(currentUrl)) { - return {}; + return { + invokeOptions: {}, + }; } } } @@ -28,7 +35,8 @@ export async function parseInvokeOptionsByOriginUrl( invokeOptions.functionDir = options.functionDir; invokeOptions.sourceDir = options.sourceDir; invokeOptions.verbose = options.verbose; - const functions = await getFuncList({ + const { functionList, invoke } = await getFuncList({ + getFunctionList: true, functionDir: options.functionDir, sourceDir: options.sourceDir, }); @@ -42,8 +50,8 @@ export async function parseInvokeOptionsByOriginUrl( }> = {}; // 获取路由 let urlMatchList = []; - Object.keys(functions).forEach(functionName => { - const functionItem = functions[functionName] || {}; + Object.keys(functionList).forEach(functionName => { + const functionItem = functionList[functionName] || {}; const httpEvents = (functionItem.events || []).filter((eventItem: any) => { return eventItem.http || eventItem.apigw; }); @@ -133,5 +141,8 @@ export async function parseInvokeOptionsByOriginUrl( invokeOptions.data = [invokeHTTPData]; } - return invokeOptions; + return { + invokeOptions, + invokeFun: invoke, + }; } diff --git a/packages/gateway-common-http/src/index.ts b/packages/gateway-common-http/src/index.ts index e9ef91faaec1..d0452cd93a38 100644 --- a/packages/gateway-common-http/src/index.ts +++ b/packages/gateway-common-http/src/index.ts @@ -23,10 +23,10 @@ export class KoaGateway implements KoaGatewayAdapter { next: () => Promise, invoke: InvokeCallback ) { - const invokeOptions = await parseInvokeOptionsByOriginUrl( - this.options, - ctx.request - ); + const { + invokeOptions, + invokeFun = invoke, + } = await parseInvokeOptionsByOriginUrl(this.options, ctx.request, invoke); if (!invokeOptions.functionName) { await next(); } else { @@ -36,7 +36,7 @@ export class KoaGateway implements KoaGatewayAdapter { statusCode: number; body: string; isBase64Encoded: boolean; - } = await invoke({ + } = await invokeFun({ functionDir: invokeOptions.functionDir, functionName: invokeOptions.functionName, data: invokeOptions.data, @@ -82,14 +82,14 @@ export class ExpressGateway implements ExpressGatewayAdapter { next: NextFunction, invoke: InvokeCallback ) { - const invokeOptions = await parseInvokeOptionsByOriginUrl( - this.options, - req - ); + const { + invokeOptions, + invokeFun = invoke, + } = await parseInvokeOptionsByOriginUrl(this.options, req, invoke); if (!invokeOptions.functionName) { return next(); } else { - invoke({ + invokeFun({ functionDir: invokeOptions.functionDir, functionName: invokeOptions.functionName, data: invokeOptions.data, diff --git a/packages/serverless-spec-builder/package.json b/packages/serverless-spec-builder/package.json index 76d0f62409c2..a095f7c460d3 100644 --- a/packages/serverless-spec-builder/package.json +++ b/packages/serverless-spec-builder/package.json @@ -7,9 +7,11 @@ "ejs": "^3.1.3", "js-yaml": "^3.13.0", "json-cycle": "^1.3.0", - "mkdirp": "^0.5.1" + "mkdirp": "^0.5.1", + "@midwayjs/decorator": "^2.0.8" }, "devDependencies": { + "fs-extra": "^8.1.0", "midway-bin": "^2.0.0", "mm": "3" }, @@ -22,6 +24,7 @@ "scf", "aws", "wrapper.ejs", + "registerFunction.js", "wrapper_app.ejs" ], "scripts": { diff --git a/packages/serverless-spec-builder/registerFunction.js b/packages/serverless-spec-builder/registerFunction.js new file mode 100644 index 000000000000..4c4aa764a0e1 --- /dev/null +++ b/packages/serverless-spec-builder/registerFunction.js @@ -0,0 +1,70 @@ +const { + saveModule, + saveProviderId, + FUNC_KEY, + attachClassMetadata, +} = require('@midwayjs/decorator'); +const { join } = require('path'); +const { existsSync } = require('fs'); +const registerFunctionToIoc = (container, funHandler, fun) => { + class Fun { + async handler(event) { + const _this = { + context: this['REQUEST_OBJ_CTX_KEY'], + event, + }; + const args = + (_this.context && + _this.context.req && + _this.context.req.body && + _this.context.req.body.args) || + []; + return fun.bind(_this)(...args); + } + } + const id = 'bind_func::' + funHandler; + saveProviderId(id, Fun, true); + container.bind(id, Fun); + saveModule(FUNC_KEY, Fun); + attachClassMetadata( + FUNC_KEY, + Object.assign({ funHandler, key: 'handler' }, {}), + Fun + ); +}; + +const registerFunctionToIocByConfig = (config, options) => { + if (!config || !config.functionList) { + return; + } + const { baseDir = process.cwd() } = options || {}; + config.functionList.forEach(functionInfo => { + const { functionName, functionFilePath, functionHandler } = functionInfo; + + if (!functionFilePath) { + return; + } + const functionPath = join(baseDir, functionFilePath); + if (!existsSync(functionPath)) { + return; + } + try { + const functionExport = require(functionPath); + if (!functionExport || !functionExport[functionName]) { + return; + } + registerFunctionToIoc( + options.context, + functionHandler || `${functionName}.handler`, + functionExport[functionName] + ); + } catch { + // + } + }); +}; + +module.exports = { + registerFunctionToIocByConfig, + registerFunctionToIoc, +}; diff --git a/packages/serverless-spec-builder/src/wrapper.ts b/packages/serverless-spec-builder/src/wrapper.ts index e9f90aaa5977..de6fb705554f 100644 --- a/packages/serverless-spec-builder/src/wrapper.ts +++ b/packages/serverless-spec-builder/src/wrapper.ts @@ -1,8 +1,7 @@ import { join, resolve } from 'path'; -import { writeFileSync, existsSync, readFileSync } from 'fs'; +import { writeFileSync, existsSync, readFileSync, copyFileSync } from 'fs'; import { render } from 'ejs'; import { getLayers } from './utils'; - // 写入口 export function writeWrapper(options: { service: any; @@ -32,9 +31,23 @@ export function writeWrapper(options: { } = options; const files = {}; + // for function programing,function + let functionMap: any; const functions = service.functions || {}; for (const func in functions) { const handlerConf = functions[func]; + // for fp + if (handlerConf.isFunctional) { + if (!functionMap?.functionList) { + functionMap = { functionList: [] }; + } + functionMap.functionList.push({ + functionName: handlerConf.exportFunction, + functionHandler: handlerConf.handler, + functionFilePath: handlerConf.sourceFilePath, + }); + } + if (handlerConf._ignore) { continue; } @@ -65,6 +78,7 @@ export function writeWrapper(options: { }); } } + const isCustomAppType = !!service?.deployType; const tpl = readFileSync( @@ -73,6 +87,15 @@ export function writeWrapper(options: { isCustomAppType ? '../wrapper_app.ejs' : '../wrapper.ejs' ) ).toString(); + + if (functionMap?.functionList?.length) { + const registerFunctionFile = join(distDir, 'registerFunction.js'); + const sourceFile = resolve(__dirname, '../registerFunction.js'); + if (!existsSync(registerFunctionFile) && existsSync(sourceFile)) { + copyFileSync(sourceFile, registerFunctionFile); + } + } + for (const file in files) { const fileName = join(distDir, `${file}.js`); const layers = getLayers(service.layers, ...files[file].originLayers); @@ -86,6 +109,7 @@ export function writeWrapper(options: { advancePreventMultiInit: advancePreventMultiInit || false, initializer: initializeName || 'initializer', handlers: files[file].handlers, + functionMap, ...layers, }); writeFileSync(fileName, content); diff --git a/packages/serverless-spec-builder/test/fixtures/wrapper/aggre.js b/packages/serverless-spec-builder/test/fixtures/wrapper/aggre.js index 48a76a1e411c..02521d799be4 100644 --- a/packages/serverless-spec-builder/test/fixtures/wrapper/aggre.js +++ b/packages/serverless-spec-builder/test/fixtures/wrapper/aggre.js @@ -1,5 +1,6 @@ const { FaaSStarter } = require('@midwayjs/faas'); const { asyncWrapper, start } = require('testStarter'); + const picomatch = require('picomatch'); @@ -15,6 +16,7 @@ const initializeMethod = async (initializeContext = {}) => { }); starter = new FaaSStarter({ baseDir: __dirname, initializeContext, applicationAdapter: runtime, middleware: ["test1","test2"] }); + await starter.start(); inited = true; }; diff --git a/packages/serverless-spec-builder/test/fixtures/wrapper/index.js b/packages/serverless-spec-builder/test/fixtures/wrapper/index.js index 6e4c69dcc277..5178240e42f3 100644 --- a/packages/serverless-spec-builder/test/fixtures/wrapper/index.js +++ b/packages/serverless-spec-builder/test/fixtures/wrapper/index.js @@ -1,5 +1,6 @@ const { FaaSStarter } = require('@midwayjs/faas'); const { asyncWrapper, start } = require('testStarter'); + const picomatch = require('picomatch'); @@ -15,6 +16,7 @@ const initializeMethod = async (initializeContext = {}) => { }); starter = new FaaSStarter({ baseDir: __dirname, initializeContext, applicationAdapter: runtime, middleware: ["test1","test2"] }); + await starter.start(); inited = true; }; diff --git a/packages/serverless-spec-builder/test/fixtures/wrapper/render.js b/packages/serverless-spec-builder/test/fixtures/wrapper/render.js index 601912a6e068..3bd25df38c11 100644 --- a/packages/serverless-spec-builder/test/fixtures/wrapper/render.js +++ b/packages/serverless-spec-builder/test/fixtures/wrapper/render.js @@ -1,5 +1,6 @@ const { FaaSStarter } = require('@midwayjs/faas'); const { asyncWrapper, start } = require('testStarter'); + const picomatch = require('picomatch'); @@ -15,6 +16,7 @@ const initializeMethod = async (initializeContext = {}) => { }); starter = new FaaSStarter({ baseDir: __dirname, initializeContext, applicationAdapter: runtime, middleware: ["test1","test2"] }); + await starter.start(); inited = true; }; diff --git a/packages/serverless-spec-builder/test/wrapper.test.ts b/packages/serverless-spec-builder/test/wrapper.test.ts index 5e9d76692514..469d7742fc81 100644 --- a/packages/serverless-spec-builder/test/wrapper.test.ts +++ b/packages/serverless-spec-builder/test/wrapper.test.ts @@ -1,12 +1,16 @@ import { writeWrapper } from '../src/wrapper'; import { resolve } from 'path'; import * as assert from 'assert'; -import { existsSync, readFileSync } from 'fs'; +import { existsSync, readFileSync, remove } from 'fs-extra'; describe('/test/wrapper.test.ts', () => { describe('test all format', () => { - it('writeWrapper', () => { + it('writeWrapper functionMap', async () => { const wrapperPath = resolve(__dirname, './fixtures/wrapper'); + const registerFunction = resolve(wrapperPath, 'registerFunction.js'); + if (existsSync(registerFunction)) { + await remove(registerFunction); + } writeWrapper({ initializeName: 'initializeUserDefine', middleware: ['test1', 'test2'], @@ -24,6 +28,9 @@ describe('/test/wrapper.test.ts', () => { }, index: { handler: 'index.handler', + isFunctional: true, + exportFunction: 'aggregation', + sourceFilePath: 'fun-index.js', }, render: { handler: 'render.handler', @@ -40,6 +47,82 @@ describe('/test/wrapper.test.ts', () => { assert(existsSync(aggrePath)); assert(existsSync(indexPath)); assert(existsSync(renderPath)); + assert(existsSync(registerFunction)); + assert( + /registerFunctionToIocByConfig/.test(readFileSync(aggrePath).toString()) + ); + assert( + /require\('registerFunction\.js'\)/.test( + readFileSync(aggrePath).toString() + ) + ); + assert( + /exports\.initializeUserDefine\s*=/.test( + readFileSync(aggrePath).toString() + ) + ); + assert( + /exports\.initializeUserDefine\s*=/.test( + readFileSync(indexPath).toString() + ) + ); + assert( + /exports\.initializeUserDefine\s*=/.test( + readFileSync(renderPath).toString() + ) + ); + await remove(registerFunction); + }); + it('writeWrapper', async () => { + const wrapperPath = resolve(__dirname, './fixtures/wrapper'); + const registerFunction = resolve(wrapperPath, 'registerFunction.js'); + if (existsSync(registerFunction)) { + await remove(registerFunction); + } + writeWrapper({ + initializeName: 'initializeUserDefine', + middleware: ['test1', 'test2'], + cover: true, + service: { + functions: { + aggregation: { + handler: 'aggre.handler', + _isAggregation: true, + functions: ['index'], + _handlers: [ + { path: '/api/test', handler: 'index.handler' }, + { path: '/*', handler: 'render.handler' }, + ], + }, + index: { + handler: 'index.handler', + }, + render: { + handler: 'render.handler', + }, + }, + }, + baseDir: wrapperPath, + distDir: wrapperPath, + starter: 'testStarter', + }); + const aggrePath = resolve(wrapperPath, 'aggre.js'); + const indexPath = resolve(wrapperPath, 'index.js'); + const renderPath = resolve(wrapperPath, 'render.js'); + assert(existsSync(aggrePath)); + assert(existsSync(indexPath)); + assert(existsSync(renderPath)); + assert(!existsSync(registerFunction)); + assert( + !/registerFunctionToIocByConfig/.test( + readFileSync(aggrePath).toString() + ) + ); + assert( + !/require\('registerFunction\.js'\)/.test( + readFileSync(aggrePath).toString() + ) + ); assert( /exports\.initializeUserDefine\s*=/.test( readFileSync(aggrePath).toString() diff --git a/packages/serverless-spec-builder/wrapper.ejs b/packages/serverless-spec-builder/wrapper.ejs index 0efd9b7a86ff..3e33fd325008 100644 --- a/packages/serverless-spec-builder/wrapper.ejs +++ b/packages/serverless-spec-builder/wrapper.ejs @@ -1,5 +1,6 @@ const { <%=faasStarterName %> } = require('<%=faasModName %>'); const { asyncWrapper, start } = require('<%=starter %>'); +<% if (functionMap) { %>const { registerFunctionToIocByConfig } = require('registerFunction.js');<% } %> const picomatch = require('picomatch'); <% layerDeps.forEach(function(layer){ %>const <%=layer.name%> = require('<%=layer.path%>'); <% }); %> @@ -17,6 +18,12 @@ const initializeMethod = async (initializeContext = {}) => { starter = new <%=faasStarterName %>({ baseDir: __dirname, initializeContext, applicationAdapter: runtime, middleware: <%-JSON.stringify(middleware)%> }); <% loadDirectory.forEach(function(dirName){ %> starter.loader.loadDirectory({ baseDir: '<%=dirName%>'});<% }) %> + <% if (functionMap) { %> + registerFunctionToIocByConfig(<%-JSON.stringify(functionMap)%>, { + baseDir: join(__dirname, 'dist'), + context: starter.loader.getApplicationContext() + }); + <% } %> await starter.start(); <% if (!advancePreventMultiInit) { %> inited = true; <% } %> };