1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
import { Project, ScriptTarget, Type, Node, SyntaxKind } from 'ts-morph'
import path from 'path'
import { v4 as uuid } from 'uuid'
import { Ctx } from './types'
import { evaluateType } from './eval'
const RESULT_TYPE_NAME = '__$result'
export interface CtxOptions {
filePath: string
}
export const createContext = (options: CtxOptions): Ctx => {
const project = new Project({
compilerOptions: {
target: ScriptTarget.ESNext,
strict: true,
alwaysStrict: true,
disableSizeLimit: true,
isolatedModules: true,
},
})
const typeChecker = project.getTypeChecker()
const sourceFile = project.addSourceFileAtPath(path.resolve(options.filePath))
const entryPoint = sourceFile.getExportedDeclarations().get('main')?.[0]
if (!entryPoint) {
throw new Error('No "main" entrypoint defined in source file')
}
const typeToString = (ty: Type | undefined): string =>
ty ? typeChecker.getTypeText(ty) : ''
const [resultTypeNode] = sourceFile.addStatements(
`type ${RESULT_TYPE_NAME} = {}`
)
const getResultExpr = (resultKey?: string) =>
`(${RESULT_TYPE_NAME}[${JSON.stringify(resultKey)})`
const addResult = (name: string, ty: string): Node | undefined =>
resultTypeNode
?.asKind(SyntaxKind.TypeAliasDeclaration)
?.getChildAtIndexIfKind(3, SyntaxKind.TypeLiteral)
?.addProperty({
name: JSON.stringify(name),
type: `{ output: ${ty} }`,
})
const createResult = (ty: string): [string, Node | undefined] => {
const resultKey = uuid()
const node = addResult(resultKey, ty)
return [resultKey, node]
}
const customEffects: Record<string, (args: Type[], ctx: Ctx) => any> = {}
const getTypeValue = (ty: Type | undefined): any => {
try {
return JSON.parse(typeToString(ty))
} catch(_) {
return null
}
}
const refMap: Map<string, string> = new Map
const createRef = (ty: string): string => {
const key = uuid()
refMap.set(key, ty)
return key
}
let currentEnv = 'node'
const setEnv = (e: string) => (currentEnv = e)
const ctx: Ctx = {
sourceFile,
typeChecker,
entryPoint,
typeToString,
getTypeValue,
get currentEnv() { return currentEnv },
setEnv,
createRef,
getRef: (k: string) => refMap.get(k),
setRef: (k: string, ty: string) => refMap.set(k, ty),
deleteRef: (k: string) => refMap.delete(k),
createResult,
getResultExpr,
printResultNode: () => console.log(resultTypeNode?.print()),
addCustomEffect: (name, expr) => {
const func = eval(expr)
Object.assign(customEffects, { [name]: func })
},
runCustomEffect: async (name, args) => {
const output = await customEffects[name]?.(args, ctx)
if (output) {
const [resultKey, _] = createResult(`${JSON.stringify(output)}`)
return [resultKey]
}
return []
},
hasCustomEffect: (name) => customEffects[name] !== undefined,
evaluateType,
}
return ctx
}
|