'use strict' // TypeScript support for function nodes, shared across platforms. The // `typescript` module is injected by the caller (`import * as tsc from // 'typescript'`) so this package needs no dependency on it; browser hosts // (monaco) reuse the same probe recipe so every platform produces identical // type strings for the engine. /** * Structural type expander prepended to probes: quick info / typeToString * would otherwise print user-declared alias/enum/interface NAMES (`Bla`, * `Something`, `Item`) which the engine's TS-type parser cannot resolve. * String-literal unions and string enums are rewritten into fresh literal * unions (mapped type with template-literal values defeats alias * preservation; the checks are non-distributive so union reassembly cannot * re-alias) and the expansion recurses through arrays and objects. Dates are * kept opaque so they don't explode into method soup. */ const FUNCTION_TYPE_EXPANDER = [ 'type __grNul = Extract;', 'type __grVal = Exclude;', 'type __grExpand = [__grVal] extends [string] ? { [K in __grVal]: `${K}` }[__grVal] | __grNul', ' : [__grVal] extends [number] ? { [K in __grVal]: K }[__grVal] | __grNul', ' : T extends Date ? Date', ' : T extends Array ? Array<__grExpand>', ' : T extends object ? (T extends infer O ? { [K in keyof O]: __grExpand } : never)', ' : T;', ].join('\n') const RESULT_MARKER = '__fnResult' // Numeric values are identical between the `typescript` module and monaco's // `monaco.languages.typescript` enums, so plain numbers work in both hosts. // target/module: ESNext, moduleResolution: NodeJs. const FUNCTION_COMPILER_OPTIONS = { target: 99, module: 99, moduleResolution: 2, allowNonTsExtensions: true, allowJs: true, checkJs: true, noEmit: true, strict: true, noImplicitAny: false, lib: ['esnext'], } const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/ const quoteKey = (key) => (IDENT.test(key) ? key : JSON.stringify(key)) /** Render a PolicyVariableType as TypeScript source for `FunctionInput`. */ const variableTypeToTs = (t) => { switch (t.type) { case 'any': return 'any' case 'null': return 'null' case 'bool': return 'boolean' case 'string': return 'string' case 'number': return 'number' case 'date': return 'string' case 'interval': return 'string' case 'const': return JSON.stringify(t.value) case 'enum': return t.values.length > 0 ? t.values.map((value) => JSON.stringify(value)).join(' | ') : 'string' case 'array': return `Array<${variableTypeToTs(t.items)}>` case 'object': { const fields = Object.entries(t.fields).map(([key, fieldType]) => { const optional = fieldType.type === 'nullable' ? '?' : '' return `${quoteKey(key)}${optional}: ${variableTypeToTs(fieldType)}` }) return fields.length > 0 ? `{ ${fields.join('; ')} }` : 'Record' } case 'nullable': return `${variableTypeToTs(t.inner)} | null` default: return 'any' } } const functionInputLib = (inputTs) => `declare type FunctionInput = ${inputTs};\n` /** * Build the type-resolution probe: the source compiled against * `FunctionInput = ` with a marker declaration whose type is the * alias-expanded awaited return type of `handler`. Hosts read the marker's * type (quick info in monaco, checker.typeToString in node). */ const buildFunctionProbe = (source, inputTs) => { const text = `type FunctionInput = ${inputTs};\n${source}\n${FUNCTION_TYPE_EXPANDER}\ndeclare const ${RESULT_MARKER}: __grExpand>>;\n${RESULT_MARKER};` return { text, marker: RESULT_MARKER, markerPosition: text.lastIndexOf(RESULT_MARKER) } } /** * Build the diagnostics probe: just the source behind the input prelude. * Diagnostics starting inside the prelude belong to the host, not the user, * and must be dropped. */ const buildFunctionCheck = (source, inputTs) => { const prelude = `type FunctionInput = ${inputTs};\n` return { text: `${prelude}${source}`, preludeLength: prelude.length } } const flattenMessage = (messageText) => typeof messageText === 'string' ? messageText : messageText.messageText /** Filter raw TS diagnostics to user-facing ones, host-agnostic. */ const filterFunctionDiagnostics = (diagnostics, preludeLength) => { const out = [] for (const diagnostic of diagnostics) { // DiagnosticCategory: Warning = 0, Error = 1 — suggestions are dropped. if (diagnostic.category !== 0 && diagnostic.category !== 1) continue if ((diagnostic.start ?? 0) < preludeLength) continue out.push({ severity: diagnostic.category === 1 ? 'error' : 'warning', message: flattenMessage(diagnostic.messageText), code: `TS${diagnostic.code}`, start: (diagnostic.start ?? preludeLength) - preludeLength, length: diagnostic.length ?? 0, }) } return out } const FILE_NAME = '/__zen_function__.ts' // Programmatic CompilerOptions need full lib FILE names ('lib.esnext.d.ts'); // the short tsconfig-style names in the shared constant are for monaco. const nodeCompilerOptions = () => ({ ...FUNCTION_COMPILER_OPTIONS, lib: FUNCTION_COMPILER_OPTIONS.lib.map((name) => (name.startsWith('lib.') ? name : `lib.${name}.d.ts`)), }) const createService = (tsc) => { const options = nodeCompilerOptions() let current = { text: '', version: 0 } const host = { getScriptFileNames: () => [FILE_NAME], getScriptVersion: (fileName) => (fileName === FILE_NAME ? String(current.version) : '1'), getScriptSnapshot: (fileName) => { if (fileName === FILE_NAME) return tsc.ScriptSnapshot.fromString(current.text) const content = tsc.sys.readFile(fileName) return content === undefined ? undefined : tsc.ScriptSnapshot.fromString(content) }, getCurrentDirectory: () => tsc.sys.getCurrentDirectory(), getCompilationSettings: () => options, getDefaultLibFileName: (opts) => tsc.getDefaultLibFilePath(opts), fileExists: (fileName) => fileName === FILE_NAME || tsc.sys.fileExists(fileName), readFile: (fileName) => (fileName === FILE_NAME ? current.text : tsc.sys.readFile(fileName)), directoryExists: tsc.sys.directoryExists, getDirectories: tsc.sys.getDirectories, } const service = tsc.createLanguageService(host, tsc.createDocumentRegistry()) return { withText(text, run) { current = { text, version: current.version + 1 } return run(service) }, } } /** * Create the synchronous function-type resolver for * `new Workspace(createTypeResolver(tsc))`. Resolves the alias-expanded * return type of each function node's `handler`, keyed and cached by * (source, inputTs). */ const createTypeResolver = (tsc) => { const runner = createService(tsc) const cache = new Map() return (source, inputType) => { const inputTs = variableTypeToTs(inputType ?? { type: 'any' }) const key = `${inputTs}${source}` if (cache.has(key)) return cache.get(key) let resolved = null try { const probe = buildFunctionProbe(source, inputTs) resolved = runner.withText(probe.text, (service) => { const program = service.getProgram() if (!program) return null const checker = program.getTypeChecker() const file = program.getSourceFile(FILE_NAME) if (!file) return null const marker = findMarker(tsc, file, probe.marker) if (!marker) return null const type = checker.getTypeAtLocation(marker) const text = checker.typeToString(type, undefined, tsc.TypeFormatFlags.NoTruncation) return text === '' || text === 'any' || text.includes('...') ? null : text }) } catch { resolved = null } cache.set(key, resolved) return resolved } } const findMarker = (tsc, file, marker) => { let found = null const visit = (node) => { if (tsc.isIdentifier(node) && node.text === marker) found = node tsc.forEachChild(node, visit) } visit(file) return found } /** * Create a diagnostics checker mirroring what the editors surface: TS * syntactic + semantic errors/warnings for a function node's source, * compiled against its resolved input type. */ const createFunctionChecker = (tsc) => { const runner = createService(tsc) return (source, inputType) => { const inputTs = typeof inputType === 'string' ? inputType : variableTypeToTs(inputType ?? { type: 'any' }) try { const check = buildFunctionCheck(source, inputTs) return runner.withText(check.text, (service) => { const diagnostics = [ ...service.getSyntacticDiagnostics(FILE_NAME), ...service.getSemanticDiagnostics(FILE_NAME), ] return filterFunctionDiagnostics(diagnostics, check.preludeLength) }) } catch { return [] } } } module.exports = { FUNCTION_TYPE_EXPANDER, FUNCTION_COMPILER_OPTIONS, variableTypeToTs, functionInputLib, buildFunctionProbe, buildFunctionCheck, filterFunctionDiagnostics, createTypeResolver, createFunctionChecker, }