// export const ts = (window as any).ts; // // @ts-ignore // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' // // @ts-ignore // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; import * as ts from 'typescript'; import { Doc, Field } from '../../fields/Doc'; import { RefField } from '../../fields/RefField'; import { ScriptField } from '../../fields/ScriptField'; import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; export { ts }; export interface ScriptSuccess { success: true; result: any; } export interface ScriptError { success: false; error: any; result: any; } export type ScriptResult = ScriptSuccess | ScriptError; export type ScriptParam = { [name: string]: string }; export interface CompiledScript { readonly compiled: true; readonly originalScript: string; readonly options: Readonly; run(args?: { [name: string]: any }, onError?: (res: any) => void, errorVal?: any): ScriptResult; } export interface CompileError { compiled: false; errors: any[]; } export type CompileResult = CompiledScript | CompileError; export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is CompileError { if ((toBeDetermined as CompileError).errors) { return true; } return false; } function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors.length) || !script) { return { compiled: false, errors }; } const paramNames = Object.keys(scriptingGlobals); const params = paramNames.map(key => scriptingGlobals[key]); // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; // let params: any[] = [Docs, ...fieldTypes]; const compiledFunction = (() => { try { return new Function(...paramNames, `return ${script}`); } catch (e) { console.log(e); return undefined; } })(); if (!compiledFunction) return { compiled: false, errors }; const { capturedVariables = {} } = options; const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => { const argsArray: any[] = []; for (const name of customParams) { if (name === 'this') { continue; } if (name in args) { argsArray.push(args[name]); } else { argsArray.push(capturedVariables[name]); } } const thisParam = args.this || capturedVariables.this; let batch: { end(): void } | undefined = undefined; try { if (!options.editable) { batch = Doc.MakeReadOnly(); } const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray); batch?.end(); return { success: true, result }; } catch (error) { batch?.end(); onError?.(script + ' ' + error); return { success: false, error, result: errorVal }; } }; return { compiled: true, run, originalScript, options }; } interface File { fileName: string; content: string; } // class ScriptingCompilerHost implements ts.CompilerHost { class ScriptingCompilerHost { files: File[] = []; // getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): ts.SourceFile | undefined { getSourceFile(fileName: string, languageVersion: any, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): any | undefined { const contents = this.readFile(fileName); if (contents !== undefined) { return ts.createSourceFile(fileName, contents, languageVersion, true); } return undefined; } // getDefaultLibFileName(options: ts.CompilerOptions): string { getDefaultLibFileName(options: any): string { return 'node_modules/typescript/lib/lib.d.ts'; // No idea what this means... } writeFile(fileName: string, content: string) { const file = this.files.find(file => file.fileName === fileName); if (file) { file.content = content; } else { this.files.push({ fileName, content }); } } getCurrentDirectory(): string { return ''; } getCanonicalFileName(fileName: string): string { return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase(); } useCaseSensitiveFileNames(): boolean { return true; } getNewLine(): string { return '\n'; } fileExists(fileName: string): boolean { return this.files.some(file => file.fileName === fileName); } readFile(fileName: string): string | undefined { const file = this.files.find(file => file.fileName === fileName); if (file) { return file.content; } return undefined; } } export type Traverser = (node: ts.Node, indentation: string) => boolean | void; export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser }; export type Transformer = { transformer: ts.TransformerFactory; getVars?: () => { [name: string]: Field }; }; export interface ScriptOptions { requiredType?: string; // does function required a typed return value addReturn?: boolean; // does the compiler automatically add a return statement params?: { [name: string]: string }; // list of function parameters and their types capturedVariables?: { [name: string]: Doc | number | string | boolean }; // list of captured variables typecheck?: boolean; // should the compiler perform typechecking editable?: boolean; // can the script edit Docs traverser?: TraverserParam; transformer?: Transformer; // does the editor display a text label by each document that can be used as a captured document reference globals?: { [name: string]: any }; } // function forEachNode(node:ts.Node, fn:(node:any) => void); function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = '') { return ( onEnter(node, indentation) || ts.forEachChild(node, (n: any) => { forEachNode(n, onEnter, onExit, indentation + ' '); }) || (onExit && onExit(node, indentation)) ); } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { const captured = options.capturedVariables ?? {}; const signature = Object.keys(captured).reduce((p, v) => { const formatCapture = (obj: any) => `${v}=${obj instanceof RefField ? 'XXX' : obj.toString()}`; if (captured[v] instanceof Array) return p + (captured[v] as any).map(formatCapture); return p + formatCapture(captured[v]); }, ''); const found = ScriptField.GetScriptFieldCache(script + ':' + signature); if (found) return found as CompiledScript; const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; if (options.params && !options.params.this) options.params.this = Doc.name; if (options.params && !options.params.self) options.params.self = Doc.name; if (options.globals) { ScriptingGlobals.setScriptingGlobals(options.globals); } const host = new ScriptingCompilerHost(); if (options.traverser) { const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); const onEnter = typeof options.traverser === 'object' ? options.traverser.onEnter : options.traverser; const onLeave = typeof options.traverser === 'object' ? options.traverser.onLeave : undefined; forEachNode(sourceFile, onEnter, onLeave); } if (options.transformer) { const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); const result = ts.transform(sourceFile, [options.transformer.transformer]); const newCaptures = options.transformer.getVars?.(); if (Object.keys(newCaptures ?? {}).length) { // tslint:disable-next-line: prefer-object-spread //options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any; const transformed = result.transformed; const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, }); script = printer.printFile(transformed[0].getSourceFile()); } result.dispose(); } const paramNames: string[] = []; if ('this' in params || 'this' in capturedVariables) { paramNames.push('this'); } for (const key in params) { if (key === 'this') continue; paramNames.push(key); } const paramList = paramNames.map(key => { const val = params[key]; return `${key}: ${val}`; }); for (const key in capturedVariables) { if (key === 'this') continue; const val = capturedVariables[key]; paramNames.push(key); paramList.push(`${key}: ${typeof val === 'object' ? Object.getPrototypeOf(val).constructor.name : typeof val}`); } const paramString = paramList.join(', '); const body = addReturn && !script.startsWith('{ return') ? `return ${script};` : script; const reqTypes = requiredType ? `: ${requiredType}` : ''; const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; host.writeFile('file.ts', funcScript); if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib.default); const program = ts.createProgram(['file.ts'], {}, host); const testResult = program.emit(); const outputText = host.readFile('file.js'); const diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); const result = Run(outputText, paramNames, diagnostics, script, options); if (options.globals) { ScriptingGlobals.resetScriptingGlobals(); } !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript); return result; } ScriptingGlobals.add(CompileScript); ScriptingGlobals.add(function runScript(doc: Doc, script: ScriptField) { return script?.script.run({ this: doc }).result; });