提交学习笔记专用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

184 lines
6.5 KiB

  1. import { isArray, isEmptyObject, isError, isMap, isPlainObject, isPrimitive, isSet, } from './is.js';
  2. import { escapeKey, stringifyPath } from './pathstringifier.js';
  3. import { isInstanceOfRegisteredClass, transformValue, untransformValue, } from './transformer.js';
  4. import { includes, forEach } from './util.js';
  5. import { parsePath } from './pathstringifier.js';
  6. import { getDeep, setDeep } from './accessDeep.js';
  7. const enableLegacyPaths = (version) => version < 1;
  8. function traverse(tree, walker, version, origin = []) {
  9. if (!tree) {
  10. return;
  11. }
  12. const legacyPaths = enableLegacyPaths(version);
  13. if (!isArray(tree)) {
  14. forEach(tree, (subtree, key) => traverse(subtree, walker, version, [
  15. ...origin,
  16. ...parsePath(key, legacyPaths),
  17. ]));
  18. return;
  19. }
  20. const [nodeValue, children] = tree;
  21. if (children) {
  22. forEach(children, (child, key) => {
  23. traverse(child, walker, version, [
  24. ...origin,
  25. ...parsePath(key, legacyPaths),
  26. ]);
  27. });
  28. }
  29. walker(nodeValue, origin);
  30. }
  31. export function applyValueAnnotations(plain, annotations, version, superJson) {
  32. traverse(annotations, (type, path) => {
  33. plain = setDeep(plain, path, v => untransformValue(v, type, superJson));
  34. }, version);
  35. return plain;
  36. }
  37. export function applyReferentialEqualityAnnotations(plain, annotations, version) {
  38. const legacyPaths = enableLegacyPaths(version);
  39. function apply(identicalPaths, path) {
  40. const object = getDeep(plain, parsePath(path, legacyPaths));
  41. identicalPaths
  42. .map(path => parsePath(path, legacyPaths))
  43. .forEach(identicalObjectPath => {
  44. plain = setDeep(plain, identicalObjectPath, () => object);
  45. });
  46. }
  47. if (isArray(annotations)) {
  48. const [root, other] = annotations;
  49. root.forEach(identicalPath => {
  50. plain = setDeep(plain, parsePath(identicalPath, legacyPaths), () => plain);
  51. });
  52. if (other) {
  53. forEach(other, apply);
  54. }
  55. }
  56. else {
  57. forEach(annotations, apply);
  58. }
  59. return plain;
  60. }
  61. const isDeep = (object, superJson) => isPlainObject(object) ||
  62. isArray(object) ||
  63. isMap(object) ||
  64. isSet(object) ||
  65. isError(object) ||
  66. isInstanceOfRegisteredClass(object, superJson);
  67. function addIdentity(object, path, identities) {
  68. const existingSet = identities.get(object);
  69. if (existingSet) {
  70. existingSet.push(path);
  71. }
  72. else {
  73. identities.set(object, [path]);
  74. }
  75. }
  76. export function generateReferentialEqualityAnnotations(identitites, dedupe) {
  77. const result = {};
  78. let rootEqualityPaths = undefined;
  79. identitites.forEach(paths => {
  80. if (paths.length <= 1) {
  81. return;
  82. }
  83. // if we're not deduping, all of these objects continue existing.
  84. // putting the shortest path first makes it easier to parse for humans
  85. // if we're deduping though, only the first entry will still exist, so we can't do this optimisation.
  86. if (!dedupe) {
  87. paths = paths
  88. .map(path => path.map(String))
  89. .sort((a, b) => a.length - b.length);
  90. }
  91. const [representativePath, ...identicalPaths] = paths;
  92. if (representativePath.length === 0) {
  93. rootEqualityPaths = identicalPaths.map(stringifyPath);
  94. }
  95. else {
  96. result[stringifyPath(representativePath)] = identicalPaths.map(stringifyPath);
  97. }
  98. });
  99. if (rootEqualityPaths) {
  100. if (isEmptyObject(result)) {
  101. return [rootEqualityPaths];
  102. }
  103. else {
  104. return [rootEqualityPaths, result];
  105. }
  106. }
  107. else {
  108. return isEmptyObject(result) ? undefined : result;
  109. }
  110. }
  111. export const walker = (object, identities, superJson, dedupe, path = [], objectsInThisPath = [], seenObjects = new Map()) => {
  112. const primitive = isPrimitive(object);
  113. if (!primitive) {
  114. addIdentity(object, path, identities);
  115. const seen = seenObjects.get(object);
  116. if (seen) {
  117. // short-circuit result if we've seen this object before
  118. return dedupe
  119. ? {
  120. transformedValue: null,
  121. }
  122. : seen;
  123. }
  124. }
  125. if (!isDeep(object, superJson)) {
  126. const transformed = transformValue(object, superJson);
  127. const result = transformed
  128. ? {
  129. transformedValue: transformed.value,
  130. annotations: [transformed.type],
  131. }
  132. : {
  133. transformedValue: object,
  134. };
  135. if (!primitive) {
  136. seenObjects.set(object, result);
  137. }
  138. return result;
  139. }
  140. if (includes(objectsInThisPath, object)) {
  141. // prevent circular references
  142. return {
  143. transformedValue: null,
  144. };
  145. }
  146. const transformationResult = transformValue(object, superJson);
  147. const transformed = transformationResult?.value ?? object;
  148. const transformedValue = isArray(transformed) ? [] : {};
  149. const innerAnnotations = {};
  150. forEach(transformed, (value, index) => {
  151. if (index === '__proto__' ||
  152. index === 'constructor' ||
  153. index === 'prototype') {
  154. throw new Error(`Detected property ${index}. This is a prototype pollution risk, please remove it from your object.`);
  155. }
  156. const recursiveResult = walker(value, identities, superJson, dedupe, [...path, index], [...objectsInThisPath, object], seenObjects);
  157. transformedValue[index] = recursiveResult.transformedValue;
  158. if (isArray(recursiveResult.annotations)) {
  159. innerAnnotations[escapeKey(index)] = recursiveResult.annotations;
  160. }
  161. else if (isPlainObject(recursiveResult.annotations)) {
  162. forEach(recursiveResult.annotations, (tree, key) => {
  163. innerAnnotations[escapeKey(index) + '.' + key] = tree;
  164. });
  165. }
  166. });
  167. const result = isEmptyObject(innerAnnotations)
  168. ? {
  169. transformedValue,
  170. annotations: !!transformationResult
  171. ? [transformationResult.type]
  172. : undefined,
  173. }
  174. : {
  175. transformedValue,
  176. annotations: !!transformationResult
  177. ? [transformationResult.type, innerAnnotations]
  178. : innerAnnotations,
  179. };
  180. if (!primitive) {
  181. seenObjects.set(object, result);
  182. }
  183. return result;
  184. };
  185. //# sourceMappingURL=plainer.js.map