Skip to content
Open
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
140 changes: 103 additions & 37 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,44 +138,110 @@ module.exports = function serialize(obj, options) {
return value;
}

function serializeFunc(fn) {
var serializedFn = fn.toString();
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
throw new TypeError('Serializing native function: ' + fn.name);
}

// pure functions, example: {key: function() {}}
if(IS_PURE_FUNCTION.test(serializedFn)) {
return serializedFn;
}

// arrow functions, example: arg1 => arg1+5
if(IS_ARROW_FUNCTION.test(serializedFn)) {
return serializedFn;
}

var argsStartsAt = serializedFn.indexOf('(');
var def = serializedFn.substr(0, argsStartsAt)
.trim()
.split(' ')
.filter(function(val) { return val.length > 0 });

var nonReservedSymbols = def.filter(function(val) {
return RESERVED_SYMBOLS.indexOf(val) === -1
});

// enhanced literal objects, example: {key() {}}
if(nonReservedSymbols.length > 0) {
return (def.indexOf('async') > -1 ? 'async ' : '') + 'function'
+ (def.join('').indexOf('*') > -1 ? '*' : '')
+ serializedFn.substr(argsStartsAt);
}

// arrow functions
return serializedFn;
function serializeFunc(fn, options) {
var serializedFn = fn.toString();
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
throw new TypeError('Serializing native function: ' + fn.name);
}

// If no space option, use original behavior
if (!options || !options.space) {
// pure functions, example: {key: function() {}}
if(IS_PURE_FUNCTION.test(serializedFn)) {
return serializedFn;
}

// arrow functions, example: arg1 => arg1+5
if(IS_ARROW_FUNCTION.test(serializedFn)) {
return serializedFn;
}

var argsStartsAt = serializedFn.indexOf('(');
var def = serializedFn.substr(0, argsStartsAt)
.trim()
.split(' ')
.filter(function(val) { return val.length > 0 });

var nonReservedSymbols = def.filter(function(val) {
return RESERVED_SYMBOLS.indexOf(val) === -1
});

// enhanced literal objects, example: {key() {}}
if(nonReservedSymbols.length > 0) {
return (def.indexOf('async') > -1 ? 'async ' : '') + 'function'
+ (def.join('').indexOf('*') > -1 ? '*' : '')
+ serializedFn.substr(argsStartsAt);
}

// arrow functions
return serializedFn;
}

// Format function with space option - much simpler approach
return formatFunctionWithSpace(serializedFn, options.space);
}

// Check if the parameter is function
function formatFunctionWithSpace(serializedFn, space) {
// Determine indent string
var indent = typeof space === 'number' ? ' '.repeat(space) : (space || ' ');
var functionIndent = indent.repeat(2); // Functions are at depth 2 (inside object)

// Find function body bounds - need to find the { that's after the parameter list
var parenDepth = 0;
var bodyStart = -1;

for (var i = 0; i < serializedFn.length; i++) {
var char = serializedFn[i];
if (char === '(') {
parenDepth++;
} else if (char === ')') {
parenDepth--;
} else if (char === '{' && parenDepth === 0) {
// This is a brace outside of parentheses, likely the function body
bodyStart = i;
break;
}
}

var bodyEnd = serializedFn.lastIndexOf('}');

if (bodyStart === -1 || bodyEnd === -1 || bodyStart >= bodyEnd) {
return serializedFn; // No function body found
}

var signature = serializedFn.substring(0, bodyStart).trim();
var body = serializedFn.substring(bodyStart + 1, bodyEnd).trim();

// Clean up signature spacing for arrow functions
if (signature.includes('=>')) {
signature = signature.replace(/\s*=>\s*/, ' => ');
}

// Handle empty body
if (!body) {
return signature + ' {\n' + functionIndent + '\n' + indent + '}';
}

// Minimal formatting: split by semicolons and add basic spacing
var statements = body.split(';').filter(function(s) { return s.trim(); });
var formattedStatements = statements.map(function(stmt) {
var trimmed = stmt.trim();

// Basic operator spacing (minimal set to avoid complexity)
trimmed = trimmed
.replace(/===(?!=)/g, ' === ')
.replace(/!==(?!=)/g, ' !== ')
.replace(/([^=])=([^=])/g, '$1 = $2')
.replace(/\|\|/g, ' || ')
.replace(/&&/g, ' && ')
.replace(/,(?!\s)/g, ', ')
.replace(/\s+/g, ' ');

return functionIndent + trimmed + (trimmed ? ';' : '');
});

return signature + ' {\n' + formattedStatements.join('\n') + '\n' + indent + '}';
} // Check if the parameter is function
if (options.ignoreFunction && typeof obj === "function") {
obj = undefined;
}
Expand Down Expand Up @@ -261,6 +327,6 @@ module.exports = function serialize(obj, options) {

var fn = functions[valueIndex];

return serializeFunc(fn);
return serializeFunc(fn, options);
});
}
64 changes: 64 additions & 0 deletions test/unit/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,70 @@ describe('serialize( obj )', function () {
'{"num":123,"str":"str"}'
);
});

it('should apply `space` option to function bodies (Issue #195)', function () {
// Test compact function without space option
var objWithFunction = {
isSupported: function({filepath}){const basename=require('path').basename(filepath);return basename===".env"||basename.startsWith(".env.")}
};

var withoutSpace = serialize(objWithFunction);
strictEqual(withoutSpace, '{"isSupported":function({filepath}){const basename=require(\'path\').basename(filepath);return basename===".env"||basename.startsWith(".env.")}}');

// Test function body should be formatted with space: 2
var withSpace2 = serialize(objWithFunction, { space: 2 });
var expected = '{\n "isSupported": function({filepath}) {\n const basename = require(\'path\').basename(filepath);\n return basename === ".env" || basename.startsWith(".env.");\n }\n}';
strictEqual(withSpace2, expected);

// Test function body should be formatted with space: 4
var withSpace4 = serialize(objWithFunction, { space: 4 });
var expectedSpace4 = '{\n "isSupported": function({filepath}) {\n const basename = require(\'path\').basename(filepath);\n return basename === ".env" || basename.startsWith(".env.");\n }\n}';
strictEqual(withSpace4, expectedSpace4);
});

it('should apply `space` option to named function bodies', function () {
var objWithNamedFunction = {
process: function processData(data) {const result=data.map(x=>x*2);if(result.length>0){return result.filter(x=>x>10);}return [];}
};

var withSpace2 = serialize(objWithNamedFunction, { space: 2 });
var expected = '{\n "process": function processData(data) {\n const result = data.map(x => x * 2);\n if (result.length > 0) {\n return result.filter(x => x > 10);\n }\n return [];\n }\n}';
strictEqual(withSpace2, expected);
});

it('should apply `space` option to arrow function bodies', function () {
var objWithArrowFunction = {
transform: (x)=>{const doubled=x*2;if(doubled>10){return doubled;}return 0;}
};

var withSpace2 = serialize(objWithArrowFunction, { space: 2 });
var expected = '{\n "transform": (x) => {\n const doubled = x * 2;\n if (doubled > 10) {\n return doubled;\n }\n return 0;\n }\n}';
strictEqual(withSpace2, expected);
});

it('should apply `space` option to multiple functions in same object', function () {
var objWithMultipleFunctions = {
fn1: function(){return 1;},
fn2: ()=>{return 2;},
fn3: function named(){return 3;}
};

var withSpace2 = serialize(objWithMultipleFunctions, { space: 2 });
var expected = '{\n "fn1": function() {\n return 1;\n },\n "fn2": () => {\n return 2;\n },\n "fn3": function named() {\n return 3;\n }\n}';
strictEqual(withSpace2, expected);
});

it('should handle edge cases with space option and functions', function () {
// Test with string space option
var objWithFunction = { fn: function(){return true;} };
var withStringSpace = serialize(objWithFunction, { space: ' ' });
var expected = '{\n "fn": function() {\n return true;\n }\n}';
strictEqual(withStringSpace, expected);

// Test with no space (should not format function bodies)
var withoutSpaceOption = serialize(objWithFunction);
strictEqual(withoutSpaceOption, '{"fn":function(){return true;}}');
});
});

describe('backwards-compatability', function () {
Expand Down