Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R|D|M|S|A|U|I|B|L)-' + UID +
// call could start past a `[native code]` match and wrongly report a native
// function as safe to serialize.
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/;
var IS_PURE_FUNCTION = /function.*?\(/;
var IS_ARROW_FUNCTION = /.*?=>.*?/;
// Classify the outer function, not a declaration or arrow inside its body.
var IS_PURE_FUNCTION = /^(?:async(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)+)?function\b/;
// A concise method named `class` is followed by `(`, unlike a class expression.
var IS_CLASS = /^class\b(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)*(?:\{|[^\s(/])/;
var IS_ARROW_FUNCTION = /^(?:async(?:\s|\/\*[\s\S]*?\*\/|\/\/[^\n]*\n)+)?(?:\([\s\S]*?\)|[^\s()]+)\s*=>/;
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
// Matches a script end tag (case-insensitive) for XSS protection: either a
// literal `</script>`, or a bare `</script` followed by one of the other
Expand Down Expand Up @@ -237,8 +240,8 @@ module.exports = function serialize(obj, options) {
serializedFn = escapeFunctionBody(serializedFn);
}

// pure functions, example: {key: function() {}}
if(IS_PURE_FUNCTION.test(serializedFn)) {
// Function and class expressions can be emitted without method conversion.
if(IS_PURE_FUNCTION.test(serializedFn) || IS_CLASS.test(serializedFn)) {
return serializedFn;
}

Expand Down
86 changes: 86 additions & 0 deletions test/unit/method-bodies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { describe, it } = require('node:test');
const { strictEqual } = require('node:assert');
const serialize = require('../../');

describe('method bodies containing other functions', function () {
it('round trips a method containing an arrow with destructured parameters', function () {
const input = {
method() {
const nested = ({ value }) => value;
return nested({ value: 42 });
}
};
const output = eval('(' + serialize(input) + ')');
strictEqual(output.method(), 42);
});

it('round trips a method containing a function declaration', function () {
const input = {
method() {
function nested() { return 42; }
return nested();
}
};
const output = eval('(' + serialize(input) + ')');
strictEqual(output.method(), 42);
});

it('preserves async methods containing arrows', async function () {
const input = {
async method() {
const nested = () => 42;
return nested();
}
};
const output = eval('(' + serialize(input) + ')');
strictEqual(await output.method(), 42);
});

it('preserves generator methods containing arrows', function () {
const input = {
*method() {
const nested = () => 42;
yield nested();
}
};
const output = eval('(' + serialize(input) + ')');
strictEqual(output.method().next().value, 42);
});
});


describe('outer function signatures', function () {
it('round trips a class with a nested arrow', function () {
class A { method() { return value => value; } }
const output = eval('(' + serialize({ A }) + ')');
strictEqual(new output.A().method()(42), 42);
strictEqual(output.A.toString(), A.toString());
});

it('round trips a class without nested functions', function () {
class A { method() { return 42; } }
const output = eval('(' + serialize({ A }) + ')');
strictEqual(new output.A().method(), 42);
strictEqual(output.A.toString(), A.toString());
});

it('still converts concise methods named class', function () {
const input = { class () { return 42; } };
const output = eval('(' + serialize(input) + ')');
strictEqual(output.class(), 42);
});

it('preserves async signatures with comments instead of spaces', async function () {
for (const fn of [async/**/function () { return 42; }, async/**/() => 42]) {
const output = eval('(' + serialize(fn) + ')');
strictEqual(await output(), 42);
strictEqual(output.constructor.name, 'AsyncFunction');
}
});

it('preserves arrows whose default arguments contain functions', function () {
const fn = (callback = function () { return 42; }) => callback();
const output = eval('(' + serialize(fn) + ')');
strictEqual(output(), 42);
});
});
Loading