Function.ts (2650B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 const createdFunctions = new Map<string, (...args: unknown[]) => unknown>(); 7 8 /** 9 * Creates a function from a string. 10 * 11 * @internal 12 */ 13 export const createFunction = ( 14 functionValue: string, 15 ): ((...args: unknown[]) => unknown) => { 16 let fn = createdFunctions.get(functionValue); 17 if (fn) { 18 return fn; 19 } 20 fn = new Function(`return ${functionValue}`)() as ( 21 ...args: unknown[] 22 ) => unknown; 23 createdFunctions.set(functionValue, fn); 24 return fn; 25 }; 26 27 /** 28 * @internal 29 */ 30 export function stringifyFunction(fn: (...args: never) => unknown): string { 31 let value = fn.toString(); 32 try { 33 new Function(`(${value})`); 34 } catch (err) { 35 if ( 36 (err as Error).message.includes( 37 `Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive`, 38 ) 39 ) { 40 // The content security policy does not allow Function eval. Let's 41 // assume the value might be valid as is. 42 return value; 43 } 44 // This means we might have a function shorthand (e.g. `test(){}`). Let's 45 // try prefixing. 46 let prefix = 'function '; 47 if (value.startsWith('async ')) { 48 prefix = `async ${prefix}`; 49 value = value.substring('async '.length); 50 } 51 value = `${prefix}${value}`; 52 try { 53 new Function(`(${value})`); 54 } catch { 55 // We tried hard to serialize, but there's a weird beast here. 56 throw new Error('Passed function cannot be serialized!'); 57 } 58 } 59 return value; 60 } 61 62 /** 63 * Replaces `PLACEHOLDER`s with the given replacements. 64 * 65 * All replacements must be valid JS code. 66 * 67 * @example 68 * 69 * ```ts 70 * interpolateFunction(() => PLACEHOLDER('test'), {test: 'void 0'}); 71 * // Equivalent to () => void 0 72 * ``` 73 * 74 * @internal 75 */ 76 export const interpolateFunction = <T extends (...args: never[]) => unknown>( 77 fn: T, 78 replacements: Record<string, string>, 79 ): T => { 80 let value = stringifyFunction(fn); 81 for (const [name, jsValue] of Object.entries(replacements)) { 82 value = value.replace( 83 new RegExp(`PLACEHOLDER\\(\\s*(?:'${name}'|"${name}")\\s*\\)`, 'g'), 84 // Wrapping this ensures tersers that accidentally inline PLACEHOLDER calls 85 // are still valid. Without, we may get calls like ()=>{...}() which is 86 // not valid. 87 `(${jsValue})`, 88 ); 89 } 90 return createFunction(value) as unknown as T; 91 }; 92 93 declare global { 94 /** 95 * Used for interpolation with {@link interpolateFunction}. 96 * 97 * @internal 98 */ 99 function PLACEHOLDER<T>(name: string): T; 100 }