/
detect-node-mutations.ts
159 lines (142 loc) · 4.18 KB
/
detect-node-mutations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import reporter from "gatsby-cli/lib/reporter"
import { getNonGatsbyCodeFrameFormatted } from "./stack-trace-utils"
import type { IGatsbyNode } from "../redux/types"
const reported = new Set<string>()
const genericProxy = createProxyHandler()
const nodeInternalProxy = createProxyHandler({
onGet(key, value) {
if (key === `fieldOwners` || key === `content`) {
// all allowed in here
return value
}
return undefined
},
onSet(target, key, value) {
if (key === `fieldOwners` || key === `content`) {
target[key] = value
return true
}
return undefined
},
})
const nodeProxy = createProxyHandler({
onGet(key, value) {
if (key === `internal`) {
return memoizedProxy(value, nodeInternalProxy)
} else if (
key === `__gatsby_resolved` ||
key === `fields` ||
key === `children`
) {
// all allowed in here
return value
}
return undefined
},
onSet(target, key, value) {
if (key === `__gatsby_resolved` || key === `fields` || key === `children`) {
target[key] = value
return true
}
return undefined
},
})
/**
* Every time we create proxy for object, we store it in WeakMap,
* so that we reuse it for that object instead of creating new Proxy.
* This also ensures reference equality: `memoizedProxy(obj) === memoizedProxy(obj)`.
* If we didn't reuse already created proxy above comparison would return false.
*/
const referenceMap = new WeakMap<any, any>()
function memoizedProxy<T>(target: T, handler: ProxyHandler<any>): T {
const alreadyWrapped = referenceMap.get(target)
if (alreadyWrapped) {
return alreadyWrapped
} else {
const wrapped = new Proxy(target, handler)
referenceMap.set(target, wrapped)
return wrapped
}
}
function createProxyHandler({
onGet,
onSet,
}: {
onGet?: (key: string | symbol, value: any) => any
onSet?: (target: any, key: string | symbol, value: any) => boolean | undefined
} = {}): ProxyHandler<any> {
function set(target, key, value): boolean {
if (onSet) {
const result = onSet(target, key, value)
if (result !== undefined) {
return result
}
}
const error = new Error(`Stack trace:`)
Error.captureStackTrace(error, set)
if (error.stack && !reported.has(error.stack)) {
reported.add(error.stack)
const codeFrame = getNonGatsbyCodeFrameFormatted({
stack: error.stack,
})
reporter.warn(
`Node mutation detected\n\n${
codeFrame ? `${codeFrame}\n\n` : ``
}${error.stack.replace(/^$Error:?\s*/, ``)}`
)
}
return true
}
function get(target, key): any {
const value = target[key]
if (onGet) {
const result = onGet(key, value)
if (result !== undefined) {
return result
}
}
const fieldDescriptor = Object.getOwnPropertyDescriptor(target, key)
if (fieldDescriptor && !fieldDescriptor.writable) {
// this is to prevent errors like:
// ```
// TypeError: 'get' on proxy: property 'constants' is a read - only and
// non - configurable data property on the proxy target but the proxy
// did not return its actual value
// (expected '[object Object]' but got '[object Object]')
// ```
return value
}
if (typeof value === `object` && value !== null) {
return memoizedProxy(value, genericProxy)
}
return value
}
return {
get,
set,
}
}
let shouldWrapNodesInProxies =
!!process.env.GATSBY_EXPERIMENTAL_DETECT_NODE_MUTATIONS
export function enableNodeMutationsDetection(): void {
shouldWrapNodesInProxies = true
reporter.warn(
`Node mutation detection is enabled. Remember to disable it after you are finished with diagnostic as it will cause build performance degradation.`
)
}
export function wrapNode<T extends IGatsbyNode | undefined>(node: T): T {
if (node && shouldWrapNodesInProxies) {
return memoizedProxy(node, nodeProxy)
} else {
return node
}
}
export function wrapNodes<T extends Array<IGatsbyNode> | undefined>(
nodes: T
): T {
if (nodes && shouldWrapNodesInProxies && nodes.length > 0) {
return nodes.map(node => memoizedProxy(node, nodeProxy)) as T
} else {
return nodes
}
}