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