├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ └── npmpublish.yml ├── .gitignore ├── .prettierrc ├── HISTORY.md ├── LICENSE ├── Readme.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── tosource.test.ts └── tosource.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@typescript-eslint/recommended", "prettier"], 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": 0, 5 | "@typescript-eslint/no-inferrable-types": 0, 6 | "@typescript-eslint/no-use-before-define": 0, 7 | "@typescript-eslint/no-parameter-properties": 0, 8 | "@typescript-eslint/explicit-function-return-type": 0, 9 | "no-var": ["error"], 10 | "prefer-const": ["error"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [10.x, 12.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm ci 20 | - run: npm run lint 21 | - run: npm run build 22 | - run: npm run test-ci 23 | - run: npx codecov 24 | env: 25 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 26 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | registry-url: https://registry.npmjs.org/ 16 | - run: npm ci 17 | - run: npm run lint 18 | - run: npm run test-ci 19 | - run: npm run build 20 | - run: npm publish --access public 21 | env: 22 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .rpt2_cache 21 | .idea 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 2.0.0-alpha.3 (2021-07-31) 2 | * **Breaking change**: Requires [`Object.is`](https://caniuse.com/mdn-javascript_builtins_object_is) or polyfill 3 | 4 | # 2.0.0-alpha.1 (2020-03-30) 5 | * **Breaking change**: Requires Node 10.x 6 | * migrate toolchain: TypeScript, prettier, eslint, jest, rollup 7 | * export TypeScript types and esm export 8 | * add support for `Map`, `Set`, negative zero, sparse arrays 9 | * serialize numeric object keys as numbers (e.g. `{'1':2}` becomes `{1:2}`) 10 | 11 | # 1.0.0 (2015-09-03) 12 | 13 | * added changelog 14 | * fixed RegExp escaping of `/` on node 0.10 15 | * added [standard](https://github.com/feross/standard) for code style/eslint 16 | 17 | # v0.1.3 (2014-10-08) 18 | 19 | * use toString for functions 20 | 21 | # v0.1.2 (2014-05-14) 22 | 23 | * fixes circular reference bug 24 | 25 | # v0.1.1 (2011-04-24) 26 | 27 | * initial release 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Marcello Bastéa-Forte (marcello@cellosoft.com) 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source 20 | distribution. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # node-tosource 2 | 3 | [![Actions Status](https://github.com/marcello3d/node-tosource/workflows/Node%20CI/badge.svg)](https://github.com/marcello3d/node-tosource/actions) 4 | [![npm version](https://badge.fury.io/js/tosource.svg)](https://badge.fury.io/js/tosource) 5 | [![codecov](https://codecov.io/gh/marcello3d/node-tosource/branch/master/graph/badge.svg)](https://codecov.io/gh/marcello3d/node-tosource) 6 | 7 | toSource is a super simple function that converts JavaScript objects back to source code. 8 | 9 | ## Introduction 10 | 11 | Motivation: JSON doesn't support serializing functions, dates, or regular expressions. I wanted 12 | a quick and simple way to push trusted data structures with code from Node down to the browser. 13 | 14 | This should make it easier to share code and modules between the server and client. 15 | 16 | ## Installation 17 | 18 | ``` 19 | npm install tosource 20 | ``` 21 | 22 | ## Examples 23 | 24 | The following code: 25 | 26 | ```js 27 | import toSource from 'tosource'; 28 | 29 | console.log( 30 | toSource([ 31 | 4, 32 | 5, 33 | 6, 34 | 'hello', 35 | { 36 | a: 2, 37 | b: 3, 38 | '1': 4, 39 | if: 5, 40 | yes: true, 41 | no: false, 42 | nan: NaN, 43 | infinity: Infinity, 44 | undefined: undefined, 45 | null: null, 46 | foo: function (bar) { 47 | console.log('woo! a is ' + a); 48 | console.log('and bar is ' + bar); 49 | }, 50 | }, 51 | /we$/gi, 52 | new Date('Wed, 09 Aug 1995 00:00:00 GMT'), 53 | ]), 54 | ); 55 | ``` 56 | 57 | Output: 58 | 59 | ``` 60 | [ 4, 61 | 5, 62 | 6, 63 | "hello", 64 | { 1:4, 65 | a:2, 66 | b:3, 67 | "if":5, 68 | yes:true, 69 | no:false, 70 | nan:NaN, 71 | infinity:Infinity, 72 | "undefined":undefined, 73 | "null":null, 74 | foo:function (bar) { 75 | console.log('woo! a is ' + a); 76 | console.log('and bar is ' + bar); 77 | } }, 78 | /we$/gi, 79 | new Date(807926400000) ] 80 | ``` 81 | 82 | See [tosource.test.ts][1] for more examples. 83 | 84 | ## Supported Types 85 | 86 | - numbers (including `NaN`, `Infinity`, and `-0`) 87 | - strings 88 | - Arrays (including sparse arrays) 89 | - object literals 90 | - function 91 | - `RegExp` instances 92 | - `Date` instances 93 | - `Map` 94 | - `Set` 95 | - `true` / `false` 96 | - `undefined` 97 | - `null` 98 | 99 | ## Notes 100 | 101 | - Functions are serialized with `func.toString()`, no closure properties are serialized 102 | - Multiple references to the same object become copies 103 | - Circular references are encoded as `{$circularReference:true}` 104 | 105 | ## License 106 | 107 | toSource is open source software under the [zlib license][2]. 108 | 109 | [1]: https://github.com/marcello3d/node-tosource/blob/master/src/tosource.test.ts 110 | [2]: https://github.com/marcello3d/node-tosource/blob/master/LICENSE 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tosource", 3 | "description": "toSource converts JavaScript objects back to source", 4 | "version": "2.0.0-alpha.3", 5 | "homepage": "https://github.com/marcello3d/node-tosource", 6 | "repository": "git://github.com/marcello3d/node-tosource.git", 7 | "author": "Marcello Bastéa-Forte (http://marcello.cellosoft.com/)", 8 | "license": "Zlib", 9 | "main": "./dist/index.js", 10 | "module": "./dist/index.esm.js", 11 | "typings": "./dist/tosource.d.ts", 12 | "keywords": [ 13 | "source", 14 | "tosource", 15 | "json", 16 | "javascript object", 17 | "object" 18 | ], 19 | "files": [ 20 | "dist/**/*", 21 | "src/**/*" 22 | ], 23 | "dependencies": {}, 24 | "scripts": { 25 | "test": "jest --watch", 26 | "test-ci": "jest --coverage", 27 | "build": "rollup -c", 28 | "lint": "eslint src/**/*.ts", 29 | "prepublishOnly": "rm -rf dist/ && npm run build" 30 | }, 31 | "engines": { 32 | "node": ">=10" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "26.0.24", 36 | "@types/node": "16.4.8", 37 | "@typescript-eslint/eslint-plugin": "4.28.5", 38 | "@typescript-eslint/parser": "4.28.5", 39 | "codecov": "3.8.3", 40 | "eslint": "7.32.0", 41 | "eslint-config-prettier": "8.3.0", 42 | "husky": "^4.2.3", 43 | "jest": "27.0.6", 44 | "prettier": "2.3.2", 45 | "pretty-quick": "3.1.1", 46 | "rollup": "2.55.1", 47 | "rollup-plugin-typescript2": "0.30.0", 48 | "ts-jest": "27.0.4", 49 | "tslint-config-prettier": "1.18.0", 50 | "typescript": "4.3.5" 51 | }, 52 | "jest": { 53 | "preset": "ts-jest", 54 | "testEnvironment": "node" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import pkg from './package.json'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | 5 | export default { 6 | input: 'src/tosource.ts', 7 | plugins: [typescript({ exclude: '**/*.test.ts' })], 8 | output: [ 9 | { 10 | file: pkg.main, 11 | sourcemap: true, 12 | sourcemapExcludeSources: true, 13 | format: 'cjs', 14 | }, 15 | { 16 | file: pkg.module, 17 | sourcemap: true, 18 | sourcemapExcludeSources: true, 19 | format: 'esm', 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/tosource.test.ts: -------------------------------------------------------------------------------- 1 | import toSource from './tosource'; 2 | 3 | // Various types 4 | describe('toSource', () => { 5 | it('works on kitchen sink', () => { 6 | const date = new Date(); 7 | const a = undefined; 8 | function foo(bar: any) { 9 | console.log('woo! a is ' + a); 10 | console.log('and bar is ' + bar); 11 | } 12 | const bar = () => 45; 13 | const v = toSource([ 14 | 0, 15 | -0, 16 | 4, 17 | 5, 18 | 6, 19 | 'hello', 20 | { 21 | 0: 1, 22 | a: 2, 23 | b: 3, 24 | '1': 4, 25 | if: 5, 26 | yes: true, 27 | no: false, 28 | nan: NaN, 29 | infinity: Infinity, 30 | undefined: undefined, 31 | null: null, 32 | foo, 33 | bar, 34 | map: new Map([ 35 | ['hello', 45], 36 | [45, 'hello'], 37 | ]), 38 | set: new Set(['hello', 45]), 39 | }, 40 | /we$/gi, 41 | new RegExp('/w/e/', 'ig'), 42 | /\/w\/e\//gim, 43 | date, 44 | new Date('Wed, 09 Aug 1995 00:00:00 GMT'), 45 | ]); 46 | 47 | expect(v).toEqual( 48 | `[ 0, 49 | -0, 50 | 4, 51 | 5, 52 | 6, 53 | "hello", 54 | { 0:1, 55 | 1:4, 56 | a:2, 57 | b:3, 58 | "if":5, 59 | yes:true, 60 | no:false, 61 | nan:NaN, 62 | infinity:Infinity, 63 | "undefined":undefined, 64 | "null":null, 65 | foo:function foo(bar) { 66 | console.log('woo! a is ' + a); 67 | console.log('and bar is ' + bar); 68 | }, 69 | bar:() => 45, 70 | map:new Map([ [ "hello", 71 | 45 ], 72 | [ 45, 73 | "hello" ] ]), 74 | set:new Set([ "hello", 75 | 45 ]) }, 76 | /we$/gi, 77 | /\\/w\\/e\\//gi, 78 | /\\/w\\/e\\//gim, 79 | new Date(${date.getTime()}), 80 | new Date(807926400000) ]`, 81 | ); 82 | }); 83 | 84 | it('zero', () => { 85 | expect(toSource(-0)).toEqual('-0'); 86 | expect(toSource(0)).toEqual('0'); 87 | }); 88 | 89 | it('sparse array', () => { 90 | expect(toSource([1, , ,], undefined, false)).toEqual('[1,,]'); 91 | }); 92 | 93 | it('sparse array 2', () => { 94 | expect(toSource([1, , , 3], undefined, false)).toEqual('[1,,,3]'); 95 | }); 96 | 97 | it('negative Infinity', () => { 98 | expect(toSource(-Infinity)).toEqual('-Infinity'); 99 | }); 100 | 101 | it('filters parameter', () => { 102 | // Filter parameter (applies to every object recursively before serializing) 103 | expect( 104 | toSource([4, 5, 6, { bar: 3 }], function numbersToStrings(value) { 105 | return typeof value === 'number' ? '<' + value + '>' : value; 106 | }), 107 | ).toEqual(`[ "<4>",\n "<5>",\n "<6>",\n { bar:"<3>" } ]`); 108 | }); 109 | 110 | it('generates with no indent', () => { 111 | expect(toSource([4, 5, 6, { bar: 3 }], undefined, false)).toEqual( 112 | '[4,5,6,{bar:3}]', 113 | ); 114 | }); 115 | 116 | it('handles circular reference', () => { 117 | const object: any = { a: 1, b: 2 }; 118 | object.c = object; 119 | 120 | expect(toSource(object)).toEqual( 121 | '{ a:1,\n' + ' b:2,\n' + ' c:{$circularReference:1} }', 122 | ); 123 | }); 124 | it('allows multiple references to the same object', () => { 125 | // Not a circular reference 126 | const foo = {}; 127 | const object = { a: foo, b: foo }; 128 | 129 | expect(toSource(object)).toEqual('{ a:{},\n' + ' b:{} }'); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/tosource.ts: -------------------------------------------------------------------------------- 1 | /* toSource by Marcello Bastea-Forte - zlib license */ 2 | export default function toSource( 3 | object: unknown, 4 | replacer?: (a: any) => any, 5 | indent: false | string = ' ', 6 | startingIndent: string = '', 7 | ): string { 8 | const seen: any[] = []; 9 | return walk( 10 | object, 11 | replacer, 12 | indent === false ? '' : indent, 13 | startingIndent, 14 | seen, 15 | ); 16 | 17 | function walk( 18 | object: any, 19 | replacer: ((a: any) => any) | undefined, 20 | indent: string, 21 | currentIndent: string, 22 | seen: any[], 23 | ): string { 24 | const nextIndent = currentIndent + indent; 25 | object = replacer ? replacer(object) : object; 26 | 27 | switch (typeof object) { 28 | case 'string': 29 | return JSON.stringify(object); 30 | case 'number': 31 | if (Object.is(object, -0)) { 32 | return '-0'; 33 | } 34 | return String(object); 35 | case 'boolean': 36 | case 'undefined': 37 | return String(object); 38 | case 'function': 39 | return object.toString(); 40 | } 41 | 42 | if (object === null) { 43 | return 'null'; 44 | } 45 | if (object instanceof RegExp) { 46 | return object.toString(); 47 | } 48 | if (object instanceof Date) { 49 | return `new Date(${object.getTime()})`; 50 | } 51 | if (object instanceof Set) { 52 | return `new Set(${walk( 53 | Array.from(object.values()), 54 | replacer, 55 | indent, 56 | nextIndent, 57 | seen, 58 | )})`; 59 | } 60 | if (object instanceof Map) { 61 | return `new Map(${walk( 62 | Array.from(object.entries()), 63 | replacer, 64 | indent, 65 | nextIndent, 66 | seen, 67 | )})`; 68 | } 69 | 70 | if (seen.indexOf(object) >= 0) { 71 | return '{$circularReference:1}'; 72 | } 73 | seen.push(object); 74 | 75 | function join(elements: any[]) { 76 | return ( 77 | indent.slice(1) + 78 | elements.join(',' + (indent && '\n') + nextIndent) + 79 | (indent ? ' ' : '') 80 | ); 81 | } 82 | 83 | if (Array.isArray(object)) { 84 | return `[${join( 85 | object.map((element) => 86 | walk(element, replacer, indent, nextIndent, seen.slice()), 87 | ), 88 | )}]`; 89 | } 90 | const keys = Object.keys(object); 91 | if (keys.length) { 92 | return `{${join( 93 | keys.map( 94 | (key) => 95 | (legalKey(key) ? key : JSON.stringify(key)) + 96 | ':' + 97 | walk(object[key], replacer, indent, nextIndent, seen.slice()), 98 | ), 99 | )}}`; 100 | } 101 | return '{}'; 102 | } 103 | } 104 | 105 | const KEYWORD_REGEXP = 106 | /^(abstract|boolean|break|byte|case|catch|char|class|const|continue|debugger|default|delete|do|double|else|enum|export|extends|false|final|finally|float|for|function|goto|if|implements|import|in|instanceof|int|interface|long|native|new|null|package|private|protected|public|return|short|static|super|switch|synchronized|this|throw|throws|transient|true|try|typeof|undefined|var|void|volatile|while|with)$/; 107 | 108 | function legalKey(key: string) { 109 | return ( 110 | /^([a-z_$][0-9a-z_$]*|[0-9]+)$/gi.test(key) && !KEYWORD_REGEXP.test(key) 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["es2015"], 7 | 8 | "rootDir": "src", 9 | "outDir": "dist", 10 | 11 | "strict": true, 12 | "alwaysStrict": true, 13 | "strictFunctionTypes": true, 14 | "strictNullChecks": true, 15 | "strictPropertyInitialization": true, 16 | "esModuleInterop": true, 17 | 18 | "forceConsistentCasingInFileNames": true, 19 | "noImplicitAny": true, 20 | "noImplicitReturns": true, 21 | "noImplicitThis": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | 26 | "declaration": true, 27 | 28 | "pretty": true 29 | }, 30 | "include": ["src/**/*"] 31 | } 32 | --------------------------------------------------------------------------------