diff --git a/src/modules/serverless/serverless.service.ts b/src/modules/serverless/serverless.service.ts index 45155904..de25682e 100644 --- a/src/modules/serverless/serverless.service.ts +++ b/src/modules/serverless/serverless.service.ts @@ -1,4 +1,9 @@ -import { Injectable, Logger } from '@nestjs/common' +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common' +import { Interval } from '@nestjs/schedule' import { isURL } from 'class-validator' import fs, { mkdir, stat } from 'fs/promises' import { cloneDeep } from 'lodash' @@ -12,7 +17,7 @@ import { UniqueArray } from '~/ts-hepler/unique' import { safePathJoin } from '~/utils' import { safeEval } from '~/utils/safe-eval.util' import { isBuiltinModule } from '~/utils/sys.util' -import type PKG from '../../../package.json' +import PKG from '../../../package.json' import { SnippetModel } from '../snippet/snippet.model' import { FunctionContextRequest, @@ -78,8 +83,6 @@ export class ServerlessService { name: model.name, reference: model.reference, - // TODO - // write file to asset writeAsset: async ( path: string, data: any, @@ -91,7 +94,6 @@ export class ServerlessService { options, ) }, - // read file to asset readAsset: async ( path: string, options: Parameters[1], @@ -99,8 +101,10 @@ export class ServerlessService { return await this.assetService.getAsset(safePathJoin(path), options) }, }, + // inject global __dirname: DATA_DIR, + __filename: '', // inject some zx utils fetch, @@ -112,108 +116,11 @@ export class ServerlessService { console: logger, logger, - require: (() => { - const __require = (id: string) => { - const module = require(id) + require: this.inNewContextRequire(), + get import() { + return this.require + }, - return cloneDeep(module) - } - - const __requireNoCache = (id: string) => { - delete require.cache[require.resolve(id)] - const module = require(id) - - return cloneDeep(module) - } - - async function $require( - this: ServerlessService, - id: string, - useCache = true, - ) { - if (!id || typeof id !== 'string') { - throw new Error('require id is not valid') - } - - // 1. if is remote module - if ( - isURL(id, { protocols: ['http', 'https'], require_protocol: true }) - ) { - const text = await this.httpService.getAndCacheRequest(id) - return await safeEval(`${text}; return module.exports`, { - exports: {}, - module: { - exports: null, - }, - }) - } - - // 2. if application third part lib - - const allowedThirdPartLibs: UniqueArray< - (keyof typeof PKG.dependencies)[] - > = [ - 'algoliasearch', - 'axios-retry', - 'axios', - 'class-transformer', - 'class-validator', - 'dayjs', - 'ejs', - 'html-minifier', - 'image-size', - 'isbot', - 'js-yaml', - 'jsdom', - 'jszip', - 'lodash', - 'marked', - 'nanoid', - 'qs', - 'rxjs', - 'snakecase-keys', - 'ua-parser-js', - 'xss', - ] - - const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-'] - - if ( - allowedThirdPartLibs.includes(id as any) || - trustPackagePrefixes.some((prefix) => id.startsWith(prefix)) - ) { - return useCache ? __require(id) : __requireNoCache(id) - } - - // 3. mock built-in module - - const mockModules = { - fs: { - writeFile: globalContext.context.writeAsset, - readFile: globalContext.context.readAsset, - }, - } - - if (Object.keys(mockModules).includes(id)) { - return mockModules[id] - } - - // fin. is built-in module - const module = isBuiltinModule(id, [ - 'fs', - 'os', - 'child_process', - 'sys', - ]) - if (!module) { - throw new Error(`cannot require ${id}`) - } else { - return __require(id) - } - } - - return $require.bind(this) - })(), process: { env: Object.freeze({ ...process.env }), nextTick: process.nextTick, @@ -226,6 +133,129 @@ export class ServerlessService { ) } + private requireModuleIdSet = new Set() + + @Interval(5 * 60 * 1000) + private cleanRequireCache() { + Array.from(this.requireModuleIdSet.values()).forEach((id) => { + delete require.cache[id] + }) + + this.requireModuleIdSet.clear() + } + private inNewContextRequire() { + const __require = (id: string) => { + const module = require(id) + // eslint-disable-next-line no-empty + if (Object.keys(PKG.dependencies).includes(id) || isBuiltinModule(id)) { + } else { + this.requireModuleIdSet.add(require.resolve(id)) + } + return cloneDeep(module) + } + + const __requireNoCache = (id: string) => { + delete require.cache[require.resolve(id)] + const clonedModule = __require(id) + + return clonedModule + } + + async function $require( + this: ServerlessService, + id: string, + useCache = true, + ) { + if (!id || typeof id !== 'string') { + throw new Error('require id is not valid') + } + + // 1. if is remote module + if (isURL(id, { protocols: ['http', 'https'], require_protocol: true })) { + let text: string + + try { + text = useCache + ? await this.httpService.getAndCacheRequest(id) + : await this.httpService.axiosRef.get(id).then((res) => res.data) + } catch (err) { + throw new InternalServerErrorException( + 'Failed to fetch remote module', + ) + } + return await safeEval( + `${text}; return module.exports ? module.exports : exports.default ? exports.default : exports`, + { + exports: {}, + module: { + exports: null, + }, + }, + ) + } + + // 2. if application third part lib + + const allowedThirdPartLibs: UniqueArray< + (keyof typeof PKG.dependencies)[] + > = [ + 'algoliasearch', + 'axios-retry', + 'axios', + 'class-transformer', + 'class-validator', + 'dayjs', + 'ejs', + 'html-minifier', + 'image-size', + 'isbot', + 'js-yaml', + 'jsdom', + 'jszip', + 'lodash', + 'marked', + 'nanoid', + 'qs', + 'rxjs', + 'snakecase-keys', + 'ua-parser-js', + 'xss', + ] + + const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-'] + + if ( + allowedThirdPartLibs.includes(id as any) || + trustPackagePrefixes.some((prefix) => id.startsWith(prefix)) + ) { + return useCache ? __require(id) : __requireNoCache(id) + } + + // 3. mock built-in module + + // const mockModules = { + // fs: { + // writeFile: globalContext.context.writeAsset, + // readFile: globalContext.context.readAsset, + // }, + // } + + // if (Object.keys(mockModules).includes(id)) { + // return mockModules[id] + // } + + // fin. is built-in module + const module = isBuiltinModule(id, ['fs', 'os', 'child_process', 'sys']) + if (!module) { + throw new Error(`cannot require ${id}`) + } else { + return __require(id) + } + } + + return $require.bind(this) + } + async isValidServerlessFunction(raw: string) { try { return safeEval(`