Reverse monkey patch built in methods to support LWC (#1509)
* Get around monkey patched Nodes
* inlineImages: Setting of `image.crossOrigin` is not always necessary (#1468)
Setting of the `crossorigin` attribute is not necessary for same-origin images, and causes an immediate image reload (albeit from cache) necessitating the use of a load event listener which subsequently mutates the snapshot. This change allows us to avoid the mutation of the snapshot for the same-origin case.
* Modify inlineImages test to remove delay and show that we can inline images without mutation
* Add an explicit test for when the `image.crossOrigin = 'anonymous';` method is necessary. Uses a combination of about:blank and our test server to simulate a cross-origin context
* Other test changes: there were some spurious rrweb mutations being generated by the addition of the crossorigin attribute that are now elimnated from the rrweb/__snapshots__/integration.test.ts.snap after this PR - this is good
* Move `childNodes` to @rrweb/utils
* Use non-monkey patched versions of the `childNodes`, `parentNode` `parentElement` `textContent` accessors
* Add getRootNode and contains, and add comprehensive todo list
* chore: Update turbo.json tasks for better build process
* Update caniuse-lite
* chore: Update eslint-plugin-compat to version 5.0.0
* chore: Bump @rrweb/utils version to 2.0.0-alpha.15
* delete unused yarn.lock files
* Set correct @rrweb/utils version in package.json
* Migrate over some accessors to reverse-monkey-patched version
* Add missing functions
* Fix illegal invocation error
* Revert closer to what it was.
This feels incorrect to me (Justin Halsall), but some of the tests break without it so I'm restoring this to be closer to its original here:
cfd686d488/packages/rrweb-snapshot/src/snapshot.ts (L1011)
* Reverse monkey patch all methods LWC hijacks
* Make tests more stable
* Safely handle rrdom nodes in hasShadowRoot
* Remove duplicated test
* Use variable `serverURL` in test
* Use monorepo default browserlist
* Fix typing issue for new typescript
* Remove unused package
* Remove unused code
* Add prefix to reverse-monkey-patched methods to make them more explicit
* Add default exports to @rrweb/utils
---------
Co-authored-by: Eoghan Murray <eoghan@getthere.ie>
This commit is contained in:
@@ -80,6 +80,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@rrweb/types": "^2.0.0-alpha.16",
|
||||
"@rrweb/utils": "^2.0.0-alpha.16",
|
||||
"@types/css-font-loading-module": "0.0.7",
|
||||
"@xstate/fsm": "^1.4.0",
|
||||
"base64-arraybuffer": "^1.0.1",
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
registerErrorHandler,
|
||||
unregisterErrorHandler,
|
||||
} from './error-handler';
|
||||
import dom from '@rrweb/utils';
|
||||
|
||||
let wrappedEmit!: (e: eventWithoutTime, isCheckout?: boolean) => void;
|
||||
|
||||
@@ -396,7 +397,8 @@ function record<T = eventWithTime>(
|
||||
stylesheetManager.trackLinkElement(n as HTMLLinkElement);
|
||||
}
|
||||
if (hasShadowRoot(n)) {
|
||||
shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
shadowDomManager.addShadowRoot(dom.shadowRoot(n as Node)!, document);
|
||||
}
|
||||
},
|
||||
onIframeLoad: (iframe, childSn) => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
getShadowHost,
|
||||
closestElementOfNode,
|
||||
} from '../utils';
|
||||
import dom from '@rrweb/utils';
|
||||
|
||||
type DoubleLinkedListNode = {
|
||||
previous: DoubleLinkedListNode | null;
|
||||
@@ -285,16 +286,13 @@ export default class MutationBuffer {
|
||||
return nextId;
|
||||
};
|
||||
const pushAdd = (n: Node) => {
|
||||
if (
|
||||
!n.parentNode ||
|
||||
!inDom(n) ||
|
||||
(n.parentNode as Element).tagName === 'TEXTAREA'
|
||||
) {
|
||||
const parent = dom.parentNode(n);
|
||||
if (!parent || !inDom(n) || (parent as Element).tagName === 'TEXTAREA') {
|
||||
return;
|
||||
}
|
||||
const parentId = isShadowRoot(n.parentNode)
|
||||
const parentId = isShadowRoot(parent)
|
||||
? this.mirror.getId(getShadowHost(n))
|
||||
: this.mirror.getId(n.parentNode);
|
||||
: this.mirror.getId(parent);
|
||||
const nextId = getNextId(n);
|
||||
if (parentId === -1 || nextId === -1) {
|
||||
return addList.addNode(n);
|
||||
@@ -326,7 +324,8 @@ export default class MutationBuffer {
|
||||
);
|
||||
}
|
||||
if (hasShadowRoot(n)) {
|
||||
this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.shadowDomManager.addShadowRoot(dom.shadowRoot(n)!, this.doc);
|
||||
}
|
||||
},
|
||||
onIframeLoad: (iframe, childSn) => {
|
||||
@@ -354,7 +353,7 @@ export default class MutationBuffer {
|
||||
for (const n of this.movedSet) {
|
||||
if (
|
||||
isParentRemoved(this.removes, n, this.mirror) &&
|
||||
!this.movedSet.has(n.parentNode!)
|
||||
!this.movedSet.has(dom.parentNode(n)!)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -378,7 +377,7 @@ export default class MutationBuffer {
|
||||
while (addList.length) {
|
||||
let node: DoubleLinkedListNode | null = null;
|
||||
if (candidate) {
|
||||
const parentId = this.mirror.getId(candidate.value.parentNode);
|
||||
const parentId = this.mirror.getId(dom.parentNode(candidate.value));
|
||||
const nextId = getNextId(candidate.value);
|
||||
if (parentId !== -1 && nextId !== -1) {
|
||||
node = candidate;
|
||||
@@ -391,7 +390,7 @@ export default class MutationBuffer {
|
||||
tailNode = tailNode.previous;
|
||||
// ensure _node is defined before attempting to find value
|
||||
if (_node) {
|
||||
const parentId = this.mirror.getId(_node.value.parentNode);
|
||||
const parentId = this.mirror.getId(dom.parentNode(_node.value));
|
||||
const nextId = getNextId(_node.value);
|
||||
|
||||
if (nextId === -1) continue;
|
||||
@@ -403,14 +402,10 @@ export default class MutationBuffer {
|
||||
// nextId !== -1 && parentId === -1 This branch can happen if the node is the child of shadow root
|
||||
else {
|
||||
const unhandledNode = _node.value;
|
||||
const parent = dom.parentNode(unhandledNode);
|
||||
// If the node is the direct child of a shadow root, we treat the shadow host as its parent node.
|
||||
if (
|
||||
unhandledNode.parentNode &&
|
||||
unhandledNode.parentNode.nodeType ===
|
||||
Node.DOCUMENT_FRAGMENT_NODE
|
||||
) {
|
||||
const shadowHost = (unhandledNode.parentNode as ShadowRoot)
|
||||
.host;
|
||||
if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||||
const shadowHost = dom.host(parent as ShadowRoot);
|
||||
const parentId = this.mirror.getId(shadowHost);
|
||||
if (parentId !== -1) {
|
||||
node = _node;
|
||||
@@ -441,12 +436,10 @@ export default class MutationBuffer {
|
||||
texts: this.texts
|
||||
.map((text) => {
|
||||
const n = text.node;
|
||||
if (
|
||||
n.parentNode &&
|
||||
(n.parentNode as Element).tagName === 'TEXTAREA'
|
||||
) {
|
||||
const parent = dom.parentNode(n);
|
||||
if (parent && (parent as Element).tagName === 'TEXTAREA') {
|
||||
// the node is being ignored as it isn't in the mirror, so shift mutation to attributes on parent textarea
|
||||
this.genTextAreaValueMutation(n.parentNode as HTMLTextAreaElement);
|
||||
this.genTextAreaValueMutation(parent as HTMLTextAreaElement);
|
||||
}
|
||||
return {
|
||||
id: this.mirror.getId(n),
|
||||
@@ -524,8 +517,8 @@ export default class MutationBuffer {
|
||||
this.attributeMap.set(textarea, item);
|
||||
}
|
||||
item.attributes.value = Array.from(
|
||||
textarea.childNodes,
|
||||
(cn) => cn.textContent || '',
|
||||
dom.childNodes(textarea),
|
||||
(cn) => dom.textContent(cn) || '',
|
||||
).join('');
|
||||
};
|
||||
|
||||
@@ -535,7 +528,7 @@ export default class MutationBuffer {
|
||||
}
|
||||
switch (m.type) {
|
||||
case 'characterData': {
|
||||
const value = m.target.textContent;
|
||||
const value = dom.textContent(m.target);
|
||||
|
||||
if (
|
||||
!isBlocked(m.target, this.blockClass, this.blockSelector, false) &&
|
||||
@@ -690,7 +683,7 @@ export default class MutationBuffer {
|
||||
m.removedNodes.forEach((n) => {
|
||||
const nodeId = this.mirror.getId(n);
|
||||
const parentId = isShadowRoot(m.target)
|
||||
? this.mirror.getId(m.target.host)
|
||||
? this.mirror.getId(dom.host(m.target))
|
||||
: this.mirror.getId(m.target);
|
||||
if (
|
||||
isBlocked(m.target, this.blockClass, this.blockSelector, false) ||
|
||||
@@ -772,9 +765,10 @@ export default class MutationBuffer {
|
||||
// if this node is blocked `serializeNode` will turn it into a placeholder element
|
||||
// but we have to remove it's children otherwise they will be added as placeholders too
|
||||
if (!isBlocked(n, this.blockClass, this.blockSelector, false)) {
|
||||
n.childNodes.forEach((childN) => this.genAdds(childN));
|
||||
dom.childNodes(n).forEach((childN) => this.genAdds(childN));
|
||||
if (hasShadowRoot(n)) {
|
||||
n.shadowRoot.childNodes.forEach((childN) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
dom.childNodes(dom.shadowRoot(n)!).forEach((childN) => {
|
||||
this.processedNodeManager.add(childN, this);
|
||||
this.genAdds(childN, n);
|
||||
});
|
||||
@@ -791,7 +785,7 @@ export default class MutationBuffer {
|
||||
*/
|
||||
function deepDelete(addsSet: Set<Node>, n: Node) {
|
||||
addsSet.delete(n);
|
||||
n.childNodes.forEach((childN) => deepDelete(addsSet, childN));
|
||||
dom.childNodes(n).forEach((childN) => deepDelete(addsSet, childN));
|
||||
}
|
||||
|
||||
function isParentRemoved(
|
||||
@@ -808,13 +802,13 @@ function _isParentRemoved(
|
||||
n: Node,
|
||||
mirror: Mirror,
|
||||
): boolean {
|
||||
let node: ParentNode | null = n.parentNode;
|
||||
let node: ParentNode | null = dom.parentNode(n);
|
||||
while (node) {
|
||||
const parentId = mirror.getId(node);
|
||||
if (removes.some((r) => r.id === parentId)) {
|
||||
return true;
|
||||
}
|
||||
node = node.parentNode;
|
||||
node = dom.parentNode(node);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -825,12 +819,12 @@ function isAncestorInSet(set: Set<Node>, n: Node): boolean {
|
||||
}
|
||||
|
||||
function _isAncestorInSet(set: Set<Node>, n: Node): boolean {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
const parent = dom.parentNode(n);
|
||||
if (!parent) {
|
||||
return false;
|
||||
}
|
||||
if (set.has(parentNode)) {
|
||||
if (set.has(parent)) {
|
||||
return true;
|
||||
}
|
||||
return _isAncestorInSet(set, parentNode);
|
||||
return _isAncestorInSet(set, parent);
|
||||
}
|
||||
|
||||
@@ -52,15 +52,7 @@ import type {
|
||||
} from '@rrweb/types';
|
||||
import MutationBuffer from './mutation';
|
||||
import { callbackWrapper } from './error-handler';
|
||||
|
||||
type WindowWithStoredMutationObserver = IWindow & {
|
||||
__rrMutationObserver?: MutationObserver;
|
||||
};
|
||||
type WindowWithAngularZone = IWindow & {
|
||||
Zone?: {
|
||||
__symbol__?: (key: string) => string;
|
||||
};
|
||||
};
|
||||
import dom, { mutationObserverCtor } from '@rrweb/utils';
|
||||
|
||||
export const mutationBuffers: MutationBuffer[] = [];
|
||||
|
||||
@@ -94,31 +86,7 @@ export function initMutationObserver(
|
||||
mutationBuffers.push(mutationBuffer);
|
||||
// see mutation.ts for details
|
||||
mutationBuffer.init(options);
|
||||
let mutationObserverCtor =
|
||||
window.MutationObserver ||
|
||||
/**
|
||||
* Some websites may disable MutationObserver by removing it from the window object.
|
||||
* If someone is using rrweb to build a browser extention or things like it, they
|
||||
* could not change the website's code but can have an opportunity to inject some
|
||||
* code before the website executing its JS logic.
|
||||
* Then they can do this to store the native MutationObserver:
|
||||
* window.__rrMutationObserver = MutationObserver
|
||||
*/
|
||||
(window as WindowWithStoredMutationObserver).__rrMutationObserver;
|
||||
const angularZoneSymbol = (
|
||||
window as WindowWithAngularZone
|
||||
)?.Zone?.__symbol__?.('MutationObserver');
|
||||
if (
|
||||
angularZoneSymbol &&
|
||||
(window as unknown as Record<string, typeof MutationObserver>)[
|
||||
angularZoneSymbol
|
||||
]
|
||||
) {
|
||||
mutationObserverCtor = (
|
||||
window as unknown as Record<string, typeof MutationObserver>
|
||||
)[angularZoneSymbol];
|
||||
}
|
||||
const observer = new (mutationObserverCtor as new (
|
||||
const observer = new (mutationObserverCtor() as new (
|
||||
callback: MutationCallback,
|
||||
) => MutationObserver)(
|
||||
callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer)),
|
||||
@@ -433,7 +401,7 @@ function initInputObserver({
|
||||
* We can treat this change as a value change of the select element the current target belongs to.
|
||||
*/
|
||||
if (target && tagName === 'OPTION') {
|
||||
target = target.parentElement;
|
||||
target = dom.parentElement(target);
|
||||
}
|
||||
if (
|
||||
!target ||
|
||||
@@ -902,7 +870,7 @@ export function initAdoptedStyleSheetObserver(
|
||||
// host of adoptedStyleSheets is outermost document or IFrame's document
|
||||
if (host.nodeName === '#document') hostId = mirror.getId(host);
|
||||
// The host is a ShadowRoot.
|
||||
else hostId = mirror.getId((host as ShadowRoot).host);
|
||||
else hostId = mirror.getId(dom.host(host as ShadowRoot));
|
||||
|
||||
const patchTarget =
|
||||
host.nodeName === '#document'
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { patch, inDom } from '../utils';
|
||||
import type { Mirror } from 'rrweb-snapshot';
|
||||
import { isNativeShadowDom } from 'rrweb-snapshot';
|
||||
import dom from '@rrweb/utils';
|
||||
|
||||
type BypassOptions = Omit<
|
||||
MutationBufferParam,
|
||||
@@ -81,7 +82,7 @@ export class ShadowDomManager {
|
||||
)
|
||||
this.bypassOptions.stylesheetManager.adoptStyleSheets(
|
||||
shadowRoot.adoptedStyleSheets,
|
||||
this.mirror.getId(shadowRoot.host),
|
||||
this.mirror.getId(dom.host(shadowRoot)),
|
||||
);
|
||||
this.restoreHandlers.push(
|
||||
initAdoptedStyleSheetObserver(
|
||||
@@ -128,13 +129,14 @@ export class ShadowDomManager {
|
||||
'attachShadow',
|
||||
function (original: (init: ShadowRootInit) => ShadowRoot) {
|
||||
return function (this: Element, option: ShadowRootInit) {
|
||||
const shadowRoot = original.call(this, option);
|
||||
const sRoot = original.call(this, option);
|
||||
// For the shadow dom elements in the document, monitor their dom mutations.
|
||||
// For shadow dom elements that aren't in the document yet,
|
||||
// we start monitoring them once their shadow dom host is appended to the document.
|
||||
if (this.shadowRoot && inDom(this))
|
||||
manager.addShadowRoot(this.shadowRoot, doc);
|
||||
return shadowRoot;
|
||||
const shadowRootEl = dom.shadowRoot(this);
|
||||
if (shadowRootEl && inDom(this))
|
||||
manager.addShadowRoot(shadowRootEl, doc);
|
||||
return sRoot;
|
||||
};
|
||||
},
|
||||
),
|
||||
|
||||
@@ -11,7 +11,8 @@ import type {
|
||||
} from '@rrweb/types';
|
||||
import type { IMirror, Mirror, SlimDOMOptions } from 'rrweb-snapshot';
|
||||
import { isShadowRoot, IGNORED_NODE, classMatchesRegex } from 'rrweb-snapshot';
|
||||
import type { RRNode, RRIFrameElement } from 'rrdom';
|
||||
import { RRNode, RRIFrameElement, BaseRRNode } from 'rrdom';
|
||||
import dom from '@rrweb/utils';
|
||||
|
||||
export function on(
|
||||
type: string,
|
||||
@@ -184,8 +185,8 @@ export function getWindowScroll(win: Window) {
|
||||
? doc.scrollingElement.scrollLeft
|
||||
: win.pageXOffset !== undefined
|
||||
? win.pageXOffset
|
||||
: doc?.documentElement.scrollLeft ||
|
||||
doc?.body?.parentElement?.scrollLeft ||
|
||||
: doc.documentElement.scrollLeft ||
|
||||
(doc?.body && dom.parentElement(doc.body)?.scrollLeft) ||
|
||||
doc?.body?.scrollLeft ||
|
||||
0,
|
||||
top: doc.scrollingElement
|
||||
@@ -193,7 +194,7 @@ export function getWindowScroll(win: Window) {
|
||||
: win.pageYOffset !== undefined
|
||||
? win.pageYOffset
|
||||
: doc?.documentElement.scrollTop ||
|
||||
doc?.body?.parentElement?.scrollTop ||
|
||||
(doc?.body && dom.parentElement(doc.body)?.scrollTop) ||
|
||||
doc?.body?.scrollTop ||
|
||||
0,
|
||||
};
|
||||
@@ -228,7 +229,7 @@ export function closestElementOfNode(node: Node | null): HTMLElement | null {
|
||||
const el: HTMLElement | null =
|
||||
node.nodeType === node.ELEMENT_NODE
|
||||
? (node as HTMLElement)
|
||||
: node.parentElement;
|
||||
: dom.parentElement(node);
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -300,17 +301,15 @@ export function isAncestorRemoved(target: Node, mirror: Mirror): boolean {
|
||||
if (!mirror.has(id)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
target.parentNode &&
|
||||
target.parentNode.nodeType === target.DOCUMENT_NODE
|
||||
) {
|
||||
const parent = dom.parentNode(target);
|
||||
if (parent && parent.nodeType === target.DOCUMENT_NODE) {
|
||||
return false;
|
||||
}
|
||||
// if the root is not document, it means the node is not in the DOM tree anymore
|
||||
if (!target.parentNode) {
|
||||
if (!parent) {
|
||||
return true;
|
||||
}
|
||||
return isAncestorRemoved(target.parentNode, mirror);
|
||||
return isAncestorRemoved(parent, mirror);
|
||||
}
|
||||
|
||||
export function legacy_isTouchEvent(
|
||||
@@ -331,24 +330,6 @@ export function polyfill(win = window) {
|
||||
win.DOMTokenList.prototype.forEach = Array.prototype
|
||||
.forEach as unknown as DOMTokenList['forEach'];
|
||||
}
|
||||
|
||||
// https://github.com/Financial-Times/polyfill-service/pull/183
|
||||
if (!Node.prototype.contains) {
|
||||
Node.prototype.contains = (...args: unknown[]) => {
|
||||
let node = args[0] as Node | null;
|
||||
if (!(0 in args)) {
|
||||
throw new TypeError('1 argument is required');
|
||||
}
|
||||
|
||||
do {
|
||||
if (this === node) {
|
||||
return true;
|
||||
}
|
||||
} while ((node = node && node.parentNode));
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ResolveTree = {
|
||||
@@ -474,7 +455,11 @@ export function getBaseDimension(
|
||||
export function hasShadowRoot<T extends Node | RRNode>(
|
||||
n: T,
|
||||
): n is T & { shadowRoot: ShadowRoot } {
|
||||
return Boolean((n as unknown as Element)?.shadowRoot);
|
||||
if (!n) return false;
|
||||
if (n instanceof BaseRRNode && 'shadowRoot' in n) {
|
||||
return Boolean(n.shadowRoot);
|
||||
}
|
||||
return Boolean(dom.shadowRoot(n as unknown as Element));
|
||||
}
|
||||
|
||||
export function getNestedRule(
|
||||
@@ -566,10 +551,11 @@ export class StyleSheetMirror {
|
||||
export function getShadowHost(n: Node): Element | null {
|
||||
let shadowHost: Element | null = null;
|
||||
if (
|
||||
n.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
|
||||
(n.getRootNode() as ShadowRoot).host
|
||||
'getRootNode' in n &&
|
||||
dom.getRootNode(n)?.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
|
||||
dom.host(dom.getRootNode(n) as ShadowRoot)
|
||||
)
|
||||
shadowHost = (n.getRootNode() as ShadowRoot).host;
|
||||
shadowHost = dom.host(dom.getRootNode(n) as ShadowRoot);
|
||||
return shadowHost;
|
||||
}
|
||||
|
||||
@@ -591,11 +577,11 @@ export function shadowHostInDom(n: Node): boolean {
|
||||
const doc = n.ownerDocument;
|
||||
if (!doc) return false;
|
||||
const shadowHost = getRootShadowHost(n);
|
||||
return doc.contains(shadowHost);
|
||||
return dom.contains(doc, shadowHost);
|
||||
}
|
||||
|
||||
export function inDom(n: Node): boolean {
|
||||
const doc = n.ownerDocument;
|
||||
if (!doc) return false;
|
||||
return doc.contains(n) || shadowHostInDom(n);
|
||||
return dom.contains(doc, n) || shadowHostInDom(n);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ async function injectRecordScript(
|
||||
} catch (e) {
|
||||
// we get this error: `Protocol error (DOM.resolveNode): Node with given id does not belong to the document`
|
||||
// then the page wasn't loaded yet and we try again
|
||||
if (!e.message.includes('DOM.resolveNode')) throw e;
|
||||
if (
|
||||
!e.message.includes('DOM.resolveNode') ||
|
||||
!e.message.includes('DOM.describeNode')
|
||||
)
|
||||
throw e;
|
||||
await injectRecordScript(frame, options);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"vite/client",
|
||||
"@types/dom-mediacapture-transform",
|
||||
"@types/offscreencanvas",
|
||||
|
||||
// rrweb specific:
|
||||
/*
|
||||
* @see https://vitest.dev/config/#globals
|
||||
@@ -18,7 +17,6 @@
|
||||
*/
|
||||
"vitest/globals"
|
||||
],
|
||||
|
||||
// TODO: enable me in the future, this is quite a large project
|
||||
// at time of writing (April 2024) there are over 100 errors in rrweb
|
||||
"strict": false
|
||||
@@ -27,6 +25,9 @@
|
||||
{
|
||||
"path": "../types"
|
||||
},
|
||||
{
|
||||
"path": "../utils"
|
||||
},
|
||||
{
|
||||
"path": "../rrdom"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user