feat: cron to clean require cache

This commit is contained in:
Innei
2022-03-11 13:17:20 +08:00
parent 0fc0b6f84b
commit 5bbdd8b2b4

View File

@@ -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<typeof fs.readFile>[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<string>()
@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(`