yahoo/serialize-javascript

Fails on native functions, like Number or String (SOLUTION INCLUDED)

TheJaredWilcurt opened this issue · 3 comments

Steps to Reproduce:

const serializeJavaScript = require('serialize-javascript');

const input = {
  type: Number
};

const output = serializeJavaScript(input);

console.log(output);

Expected:

'{"type":Number}'

Actual:

Uncaught TypeError: Serializing native function: Number
    at serializeFunc (/node_modules/serialize-javascript/index.js:146:17)
    at /node_modules/serialize-javascript/index.js:266:16
    at String.replace (<anonymous>)
    at serialize (/node_modules/serialize-javascript/index.js:220:16)

Solution:

const x = Number;
console.log(x === Number); // true

console.log('' + Number); // 'function Number() { [native code] }'

Since you can check for native constructors, and can combine them with an empty string to get access to their name in a predictable position in a new string, a conditional formatter could be added:

const getNativeBuiltInName = function (value) {
  if (typeof(value) !== 'function') {
    return;
  }
  /**
   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
   * Note: Not all global objects listed there can be combined when an empty string to
   * produce a native function string, needed in the following condition.
   */
  const nativeConstructorsAsStrings = [
    'AggregateError',
    'Array',
    'ArrayBuffer',
    'BigInt',
    'BigInt64Array',
    'BigUint64Array',
    'Boolean',
    'DataView',
    'Date',
    'Error',
    'EvalError',
    'FinalizationRegistry',
    'Float32Array',
    'Float64Array',
    'Function',
    'Int8Array',
    'Int16Array',
    'Int32Array',
    'Intl.Collator',
    'Intl.DateTimeFormat',
    'Intl.DisplayNames',
    'Intl.ListFormat',
    'Intl.Locale',
    'Intl.NumberFormat',
    'Intl.PluralRules',
    'Intl.RelativeTimeFormat',
    'Intl.Segmenter',
    'Iterator',
    'Map',
    'Number',
    'Object',
    'Promise',
    'Proxy',
    'RangeError',
    'ReferenceError',
    'RegExp',
    'Set',
    'SharedArrayBuffer',
    'String',
    'Symbol',
    'SyntaxError',
    'TypeError',
    'Uint8Array',
    'Uint8ClampedArray',
    'Uint16Array',
    'Uint32Array',
    'URIError',
    'WeakMap',
    'WeakRef',
    'WeakSet'
  ];
  const nativeConstructors = [];

  /**
   * Not all native constructors exist in all environments, some browsers or older Node
   * versions may not support everything. So here we check the window/global object
   * for their existence before pushing them onto the array for use in this function.
   * This prevents run-time bombs from popping up, and the need to meticulously document
   * the varied support of these features. However, to support both browsers and Node, we
   * use globalThis, which has wide support among Node/Browsers since 2019. But if you are
   * targeting legacy environments, you'd need to replace it with global or window.
   */
  for (const nativeConstructor of nativeConstructorsAsStrings) {
    if (globalThis[nativeConstructor]) {
      nativeConstructors.push(globalThis[nativeConstructor]);
    }
  }

  if (nativeConstructors.includes(value)) {
    // Number => 'function Number() { [native code] }' => 'Number'
    return (value + '').split(' ')[1].split('(')[0];
  }
  return;
};

With this all native constructors get turned into a string of their Name, and everything else is ignored and returned as undefined.

  • getNativeBuiltInName(Boolean) - returns 'Boolean'
  • getNativeBuiltInName(Number) - returns 'Number'
  • getNativeBuiltInName(String) - returns 'String'
  • getNativeBuiltInName(25) - returns undefined
  • getNativeBuiltInName('potato') - returns undefined

If you prefer a harder to read, but marginally faster to execute regex approach for the last conditional:

if (nativeConstructors.includes(value)) {
  // Number => 'function Number() { [native code] }'
  const functionString = '' + value;
  const hasName = functionString.match(/function\s(\w+)\(\)/);
  if (hasName) {
    // 'Number'
    const name = hasName[1];
    return name;
  }
}

I'll leave this to @redonkulus to figure out the best way to implement this solution. Since this repo relies on Node specific stuff (Buffer), and my use case needs to work in a browser I just wrote my own serializer for my project.