UNPKG

7.57 kBJavaScriptView Raw
1'use strict';
2
3const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
4const unsafeChars = /[<>\b\f\n\r\t\0\u2028\u2029]/g;
5const reserved = /^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;
6const escaped = {
7 "<": "\\u003C",
8 ">": "\\u003E",
9 "/": "\\u002F",
10 "\\": "\\\\",
11 "\b": "\\b",
12 "\f": "\\f",
13 "\n": "\\n",
14 "\r": "\\r",
15 " ": "\\t",
16 "\0": "\\0",
17 "\u2028": "\\u2028",
18 "\u2029": "\\u2029"
19};
20const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join("\0");
21function devalue(value) {
22 const counts = new Map();
23 let logNum = 0;
24 function log(message) {
25 if (logNum < 100) {
26 console.warn(message);
27 logNum += 1;
28 }
29 }
30 function walk(thing) {
31 if (typeof thing === "function") {
32 log(`Cannot stringify a function ${thing.name}`);
33 return;
34 }
35 if (counts.has(thing)) {
36 counts.set(thing, counts.get(thing) + 1);
37 return;
38 }
39 counts.set(thing, 1);
40 if (!isPrimitive(thing)) {
41 const type = getType(thing);
42 switch (type) {
43 case "Number":
44 case "String":
45 case "Boolean":
46 case "Date":
47 case "RegExp":
48 return;
49 case "Array":
50 thing.forEach(walk);
51 break;
52 case "Set":
53 case "Map":
54 Array.from(thing).forEach(walk);
55 break;
56 default:
57 const proto = Object.getPrototypeOf(thing);
58 if (proto !== Object.prototype && proto !== null && Object.getOwnPropertyNames(proto).sort().join("\0") !== objectProtoOwnPropertyNames) {
59 if (typeof thing.toJSON !== "function") {
60 log(`Cannot stringify arbitrary non-POJOs ${thing.constructor.name}`);
61 }
62 } else if (Object.getOwnPropertySymbols(thing).length > 0) {
63 log(`Cannot stringify POJOs with symbolic keys ${Object.getOwnPropertySymbols(thing).map((symbol) => symbol.toString())}`);
64 } else {
65 Object.keys(thing).forEach((key) => walk(thing[key]));
66 }
67 }
68 }
69 }
70 walk(value);
71 const names = new Map();
72 Array.from(counts).filter((entry) => entry[1] > 1).sort((a, b) => b[1] - a[1]).forEach((entry, i) => {
73 names.set(entry[0], getName(i));
74 });
75 function stringify(thing) {
76 if (names.has(thing)) {
77 return names.get(thing);
78 }
79 if (isPrimitive(thing)) {
80 return stringifyPrimitive(thing);
81 }
82 const type = getType(thing);
83 switch (type) {
84 case "Number":
85 case "String":
86 case "Boolean":
87 return `Object(${stringify(thing.valueOf())})`;
88 case "RegExp":
89 return thing.toString();
90 case "Date":
91 return `new Date(${thing.getTime()})`;
92 case "Array":
93 const members = thing.map((v, i) => i in thing ? stringify(v) : "");
94 const tail = thing.length === 0 || thing.length - 1 in thing ? "" : ",";
95 return `[${members.join(",")}${tail}]`;
96 case "Set":
97 case "Map":
98 return `new ${type}([${Array.from(thing).map(stringify).join(",")}])`;
99 default:
100 if (thing.toJSON) {
101 let json = thing.toJSON();
102 if (getType(json) === "String") {
103 try {
104 json = JSON.parse(json);
105 } catch (e) {
106 }
107 }
108 return stringify(json);
109 }
110 if (Object.getPrototypeOf(thing) === null) {
111 if (Object.keys(thing).length === 0) {
112 return "Object.create(null)";
113 }
114 return `Object.create(null,{${Object.keys(thing).map((key) => `${safeKey(key)}:{writable:true,enumerable:true,value:${stringify(thing[key])}}`).join(",")}})`;
115 }
116 return `{${Object.keys(thing).map((key) => `${safeKey(key)}:${stringify(thing[key])}`).join(",")}}`;
117 }
118 }
119 const str = stringify(value);
120 if (names.size) {
121 const params = [];
122 const statements = [];
123 const values = [];
124 names.forEach((name, thing) => {
125 params.push(name);
126 if (isPrimitive(thing)) {
127 values.push(stringifyPrimitive(thing));
128 return;
129 }
130 const type = getType(thing);
131 switch (type) {
132 case "Number":
133 case "String":
134 case "Boolean":
135 values.push(`Object(${stringify(thing.valueOf())})`);
136 break;
137 case "RegExp":
138 values.push(thing.toString());
139 break;
140 case "Date":
141 values.push(`new Date(${thing.getTime()})`);
142 break;
143 case "Array":
144 values.push(`Array(${thing.length})`);
145 thing.forEach((v, i) => {
146 statements.push(`${name}[${i}]=${stringify(v)}`);
147 });
148 break;
149 case "Set":
150 values.push("new Set");
151 statements.push(`${name}.${Array.from(thing).map((v) => `add(${stringify(v)})`).join(".")}`);
152 break;
153 case "Map":
154 values.push("new Map");
155 statements.push(`${name}.${Array.from(thing).map(([k, v]) => `set(${stringify(k)}, ${stringify(v)})`).join(".")}`);
156 break;
157 default:
158 values.push(Object.getPrototypeOf(thing) === null ? "Object.create(null)" : "{}");
159 Object.keys(thing).forEach((key) => {
160 statements.push(`${name}${safeProp(key)}=${stringify(thing[key])}`);
161 });
162 }
163 });
164 statements.push(`return ${str}`);
165 return `(function(${params.join(",")}){${statements.join(";")}}(${values.join(",")}))`;
166 } else {
167 return str;
168 }
169}
170function getName(num) {
171 let name = "";
172 do {
173 name = chars[num % chars.length] + name;
174 num = ~~(num / chars.length) - 1;
175 } while (num >= 0);
176 return reserved.test(name) ? `${name}0` : name;
177}
178function isPrimitive(thing) {
179 return Object(thing) !== thing;
180}
181function stringifyPrimitive(thing) {
182 if (typeof thing === "string") {
183 return stringifyString(thing);
184 }
185 if (thing === void 0) {
186 return "void 0";
187 }
188 if (thing === 0 && 1 / thing < 0) {
189 return "-0";
190 }
191 const str = String(thing);
192 if (typeof thing === "number") {
193 return str.replace(/^(-)?0\./, "$1.");
194 }
195 return str;
196}
197function getType(thing) {
198 return Object.prototype.toString.call(thing).slice(8, -1);
199}
200function escapeUnsafeChar(c) {
201 return escaped[c] || c;
202}
203function escapeUnsafeChars(str) {
204 return str.replace(unsafeChars, escapeUnsafeChar);
205}
206function safeKey(key) {
207 return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? key : escapeUnsafeChars(JSON.stringify(key));
208}
209function safeProp(key) {
210 return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(key) ? `.${key}` : `[${escapeUnsafeChars(JSON.stringify(key))}]`;
211}
212function stringifyString(str) {
213 let result = '"';
214 for (let i = 0; i < str.length; i += 1) {
215 const char = str.charAt(i);
216 const code = char.charCodeAt(0);
217 if (char === '"') {
218 result += '\\"';
219 } else if (char in escaped) {
220 result += escaped[char];
221 } else if (code >= 55296 && code <= 57343) {
222 const next = str.charCodeAt(i + 1);
223 if (code <= 56319 && (next >= 56320 && next <= 57343)) {
224 result += char + str[++i];
225 } else {
226 result += `\\u${code.toString(16).toUpperCase()}`;
227 }
228 } else {
229 result += char;
230 }
231 }
232 result += '"';
233 return result;
234}
235
236module.exports = devalue;