diff --git a/index.js b/index.js index e8022a3..ea05ea4 100644 --- a/index.js +++ b/index.js @@ -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; } @@ -261,6 +327,6 @@ module.exports = function serialize(obj, options) { var fn = functions[valueIndex]; - return serializeFunc(fn); + return serializeFunc(fn, options); }); } diff --git a/test/unit/serialize.js b/test/unit/serialize.js index 62c0eee..697ab9c 100644 --- a/test/unit/serialize.js +++ b/test/unit/serialize.js @@ -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 () {