aboutsummaryrefslogtreecommitdiff
path: root/src/context.ts
blob: 485083add4fd62e7bcbb7115a7e15455fc81f4d0 (plain) (blame)
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
}