diff --git a/package.json b/package.json index 255cf2fcab..ffe7c81479 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "prettier": "2.8.4", "size-limit": "~8.2.6", "turbo": "^1.2.4", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "yalc": "^1.0.0-pre.53" }, "scripts": { "build:all": "NODE_OPTIONS='--max-old-space-size=4096' yarn run concurrently --success=all -r -m=1 'yarn workspaces-to-typescript-project-references' 'yarn turbo run prepare'", diff --git a/packages/rrweb-player/src/main.js b/packages/rrweb-player/src/main.js new file mode 100644 index 0000000000..071d03bc9a --- /dev/null +++ b/packages/rrweb-player/src/main.js @@ -0,0 +1,11 @@ +import _Player from './Player.svelte'; +class Player extends _Player { + constructor(options) { + super({ + target: options.target, + props: options.data || options.props, + }); + } +} +export default Player; +//# sourceMappingURL=main.js.map \ No newline at end of file diff --git a/packages/rrweb-player/src/main.js.map b/packages/rrweb-player/src/main.js.map new file mode 100644 index 0000000000..cbeaff0a72 --- /dev/null +++ b/packages/rrweb-player/src/main.js.map @@ -0,0 +1 @@ +{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,iBAAiB,CAAC;AAMtC,MAAM,MAAO,SAAQ,OAAO;IAC1B,YAAY,OAKX;QACC,KAAK,CAAC;YACJ,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,KAAK,EAAE,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK;SACrC,CAAC,CAAC;IACL,CAAC;CACF;AAED,eAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/packages/rrweb-player/src/utils.js b/packages/rrweb-player/src/utils.js new file mode 100644 index 0000000000..9f88201d92 --- /dev/null +++ b/packages/rrweb-player/src/utils.js @@ -0,0 +1,146 @@ +import { EventType, IncrementalSource } from '@sentry-internal/rrweb'; +export function inlineCss(cssObj) { + let style = ''; + Object.keys(cssObj).forEach((key) => { + style += `${key}: ${cssObj[key]};`; + }); + return style; +} +function padZero(num, len = 2) { + let str = String(num); + const threshold = Math.pow(10, len - 1); + if (num < threshold) { + while (String(threshold).length > str.length) { + str = `0${num}`; + } + } + return str; +} +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; +export function formatTime(ms) { + if (ms <= 0) { + return '00:00'; + } + const hour = Math.floor(ms / HOUR); + ms = ms % HOUR; + const minute = Math.floor(ms / MINUTE); + ms = ms % MINUTE; + const second = Math.floor(ms / SECOND); + if (hour) { + return `${padZero(hour)}:${padZero(minute)}:${padZero(second)}`; + } + return `${padZero(minute)}:${padZero(second)}`; +} +export function openFullscreen(el) { + if (el.requestFullscreen) { + return el.requestFullscreen(); + } + else if (el.mozRequestFullScreen) { + /* Firefox */ + return el.mozRequestFullScreen(); + } + else if (el.webkitRequestFullscreen) { + /* Chrome, Safari and Opera */ + return el.webkitRequestFullscreen(); + } + else if (el.msRequestFullscreen) { + /* IE/Edge */ + return el.msRequestFullscreen(); + } +} +export function exitFullscreen() { + if (document.exitFullscreen) { + return document.exitFullscreen(); + } + else if (document.mozExitFullscreen) { + /* Firefox */ + return document.mozExitFullscreen(); + } + else if (document.webkitExitFullscreen) { + /* Chrome, Safari and Opera */ + return document.webkitExitFullscreen(); + } + else if (document.msExitFullscreen) { + /* IE/Edge */ + return document.msExitFullscreen(); + } +} +export function isFullscreen() { + let fullscreen = false; + [ + 'fullscreen', + 'webkitIsFullScreen', + 'mozFullScreen', + 'msFullscreenElement', + ].forEach((fullScreenAccessor) => { + if (fullScreenAccessor in document) { + fullscreen = fullscreen || Boolean(document[fullScreenAccessor]); + } + }); + return fullscreen; +} +export function onFullscreenChange(handler) { + document.addEventListener('fullscreenchange', handler); + document.addEventListener('webkitfullscreenchange', handler); + document.addEventListener('mozfullscreenchange', handler); + document.addEventListener('MSFullscreenChange', handler); + return () => { + document.removeEventListener('fullscreenchange', handler); + document.removeEventListener('webkitfullscreenchange', handler); + document.removeEventListener('mozfullscreenchange', handler); + document.removeEventListener('MSFullscreenChange', handler); + }; +} +export function typeOf(obj) { + // eslint-disable-next-line @typescript-eslint/unbound-method + const toString = Object.prototype.toString; + const map = { + '[object Boolean]': 'boolean', + '[object Number]': 'number', + '[object String]': 'string', + '[object Function]': 'function', + '[object Array]': 'array', + '[object Date]': 'date', + '[object RegExp]': 'regExp', + '[object Undefined]': 'undefined', + '[object Null]': 'null', + '[object Object]': 'object', + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return map[toString.call(obj)]; +} +/** + * Forked from 'rrweb' replay/index.ts. The original function is not exported. + * Determine whether the event is a user interaction event + * @param event - event to be determined + * @returns true if the event is a user interaction event + */ +function isUserInteraction(event) { + if (event.type !== EventType.IncrementalSnapshot) { + return false; + } + return (event.data.source > IncrementalSource.Mutation && + event.data.source <= IncrementalSource.Input); +} +/** + * Get periods of time when no user interaction happened from a list of events. + * @param events - all events + * @param inactivePeriodThreshold - threshold of inactive time in milliseconds + * @returns periods of time consist with [start time, end time] + */ +export function getInactivePeriods(events, inactivePeriodThreshold) { + const inactivePeriods = []; + let lastActiveTime = events[0].timestamp; + for (const event of events) { + if (!isUserInteraction(event)) + continue; + if (event.timestamp - lastActiveTime > inactivePeriodThreshold) { + inactivePeriods.push([lastActiveTime, event.timestamp]); + } + lastActiveTime = event.timestamp; + } + return inactivePeriods; +} +//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/packages/rrweb-player/src/utils.js.map b/packages/rrweb-player/src/utils.js.map new file mode 100644 index 0000000000..b01d6d1ec0 --- /dev/null +++ b/packages/rrweb-player/src/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","sourceRoot":"","sources":["utils.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/rrweb-player/typings/src/main.d.ts b/packages/rrweb-player/typings/src/main.d.ts new file mode 100644 index 0000000000..220e45aefa --- /dev/null +++ b/packages/rrweb-player/typings/src/main.d.ts @@ -0,0 +1,13 @@ +import type { eventWithTime } from '@sentry-internal/rrweb-types'; +import _Player from './Player.svelte'; +type PlayerProps = { + events: eventWithTime[]; +}; +declare class Player extends _Player { + constructor(options: { + target: Element; + props: PlayerProps; + data?: PlayerProps; + }); +} +export default Player; diff --git a/packages/rrweb-player/typings/src/utils.d.ts b/packages/rrweb-player/typings/src/utils.d.ts new file mode 100644 index 0000000000..150b588bac --- /dev/null +++ b/packages/rrweb-player/typings/src/utils.d.ts @@ -0,0 +1,30 @@ +declare global { + interface Document { + mozExitFullscreen: Document['exitFullscreen']; + webkitExitFullscreen: Document['exitFullscreen']; + msExitFullscreen: Document['exitFullscreen']; + webkitIsFullScreen: Document['fullscreen']; + mozFullScreen: Document['fullscreen']; + msFullscreenElement: Document['fullscreen']; + } + interface HTMLElement { + mozRequestFullScreen: Element['requestFullscreen']; + webkitRequestFullscreen: Element['requestFullscreen']; + msRequestFullscreen: Element['requestFullscreen']; + } +} +import type { eventWithTime } from '@sentry-internal/rrweb-types'; +export declare function inlineCss(cssObj: Record): string; +export declare function formatTime(ms: number): string; +export declare function openFullscreen(el: HTMLElement): Promise; +export declare function exitFullscreen(): Promise; +export declare function isFullscreen(): boolean; +export declare function onFullscreenChange(handler: () => unknown): () => void; +export declare function typeOf(obj: unknown): 'boolean' | 'number' | 'string' | 'function' | 'array' | 'date' | 'regExp' | 'undefined' | 'null' | 'object'; +/** + * Get periods of time when no user interaction happened from a list of events. + * @param events - all events + * @param inactivePeriodThreshold - threshold of inactive time in milliseconds + * @returns periods of time consist with [start time, end time] + */ +export declare function getInactivePeriods(events: eventWithTime[], inactivePeriodThreshold: number): [number, number][]; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 43747e12dd..d03d69729b 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -123,9 +123,7 @@ export class Replayer { public usingVirtualDom = false; public virtualDom: RRDocument = new RRDocument(); - private mouse: HTMLDivElement; private mouseTail: HTMLCanvasElement | null = null; - private tailPositions: Array<{ x: number; y: number }> = []; private emitter: Emitter = mitt(); @@ -148,8 +146,16 @@ export class Replayer { private newDocumentQueue: addedNodeMutation[] = []; - private mousePos: mouseMovePos | null = null; - private touchActive: boolean | null = null; + // Map of touch ID to the unique vars used to show gestures + private multiTouchMap: Record< + number, + { + touchActive: boolean | null; + mouse: HTMLDivElement; + tailPositions: Array<{ x: number; y: number }>; + mousePos: mouseMovePos | null; + } + > = {}; private lastMouseDownEvent: [Node, Event] | null = null; // Keep the rootNode of the last hovered element. So when hovering a new element, we can remove the last hovered element's :hover style. @@ -293,23 +299,27 @@ export class Replayer { this.adoptedStyleSheets = []; } - if (this.mousePos) { - this.moveAndHover( - this.mousePos.x, - this.mousePos.y, - this.mousePos.id, - true, - this.mousePos.debugData, - ); - this.mousePos = null; - } - - if (this.touchActive === true) { - this.mouse.classList.add('touch-active'); - } else if (this.touchActive === false) { - this.mouse.classList.remove('touch-active'); + for (const [touchId, value] of Object.entries(this.multiTouchMap)) { + const { mouse } = value; + let { mousePos, touchActive } = value; + if (touchActive === true) { + mouse.classList.add('touch-active'); + } else if (touchActive === false) { + mouse.classList.remove('touch-active'); + } + touchActive = null; + if (mousePos) { + this.moveAndHover( + mousePos.x, + mousePos.y, + mousePos.id, + true, + mousePos.debugData, + parseInt(touchId), + ); + mousePos = null; + } } - this.touchActive = null; if (this.lastMouseDownEvent) { const [target, event] = this.lastMouseDownEvent; @@ -402,9 +412,16 @@ export class Replayer { ); }, 1); } - if (this.service.state.context.events.find(indicatesTouchDevice)) { - this.mouse.classList.add('touch-device'); - } + this.service.state.context.events.forEach((e: eventWithTime) => { + if (indicatesTouchDevice(e)) { + const d = e.data as incrementalData; + const touchId = + 'touchId' in d && d.touchId !== undefined + ? d.touchId + : Object.keys(this.multiTouchMap).length; + this.multiTouchMap[touchId].mouse.classList.add('touch-device'); + } + }); } public on(event: string, handler: Handler) { @@ -542,7 +559,12 @@ export class Replayer { ? this.config.unpackFn(rawEvent as string) : (rawEvent as eventWithTime); if (indicatesTouchDevice(event)) { - this.mouse.classList.add('touch-device'); + const d = event.data as incrementalData; + const touchId = + 'touchId' in d && d.touchId !== undefined + ? d.touchId + : Object.keys(this.multiTouchMap).length; + this.multiTouchMap[touchId].mouse.classList.add('touch-device'); } void Promise.resolve().then(() => this.service.send({ type: 'ADD_EVENT', payload: { event } }), @@ -572,9 +594,11 @@ export class Replayer { this.wrapper.classList.add('replayer-wrapper'); this.config.root.appendChild(this.wrapper); - this.mouse = document.createElement('div'); - this.mouse.classList.add('replayer-mouse'); - this.wrapper.appendChild(this.mouse); + for (let [, { mouse }] of Object.entries(this.multiTouchMap)) { + mouse = document.createElement('div'); + mouse.classList.add('replayer-mouse'); + this.wrapper.appendChild(mouse); + } if (this.config.mouseTail !== false) { this.mouseTail = document.createElement('canvas'); @@ -1085,10 +1109,14 @@ export class Replayer { } case IncrementalSource.Drag: case IncrementalSource.TouchMove: - case IncrementalSource.MouseMove: + case IncrementalSource.MouseMove: { + const touchId = + 'touchId' in d && d.touchId !== undefined + ? d.touchId + : Object.keys(this.multiTouchMap).length; if (isSync) { const lastPosition = d.positions[d.positions.length - 1]; - this.mousePos = { + this.multiTouchMap[touchId].mousePos = { x: lastPosition.x, y: lastPosition.y, id: lastPosition.id, @@ -1098,7 +1126,7 @@ export class Replayer { d.positions.forEach((p) => { const action = { doAction: () => { - this.moveAndHover(p.x, p.y, p.id, isSync, d); + this.moveAndHover(p.x, p.y, p.id, isSync, d, touchId); }, delay: p.timeOffset + @@ -1117,7 +1145,9 @@ export class Replayer { }); } break; + } case IncrementalSource.MouseInteraction: { + const touchId = d.touchId ?? Object.keys(this.multiTouchMap).length; /** * Same as the situation of missing input target. */ @@ -1154,16 +1184,16 @@ export class Replayer { case MouseInteractions.MouseUp: if (isSync) { if (d.type === MouseInteractions.TouchStart) { - this.touchActive = true; + this.multiTouchMap[touchId].touchActive = true; } else if (d.type === MouseInteractions.TouchEnd) { - this.touchActive = false; + this.multiTouchMap[touchId].touchActive = false; } if (d.type === MouseInteractions.MouseDown) { this.lastMouseDownEvent = [target, event]; } else if (d.type === MouseInteractions.MouseUp) { this.lastMouseDownEvent = null; } - this.mousePos = { + this.multiTouchMap[touchId].mousePos = { x: d.x || 0, y: d.y || 0, id: d.id, @@ -1172,9 +1202,9 @@ export class Replayer { } else { if (d.type === MouseInteractions.TouchStart) { // don't draw a trail as user has lifted finger and is placing at a new point - this.tailPositions.length = 0; + this.multiTouchMap[touchId].tailPositions.length = 0; } - this.moveAndHover(d.x || 0, d.y || 0, d.id, isSync, d); + this.moveAndHover(d.x || 0, d.y || 0, d.id, isSync, d, touchId); if (d.type === MouseInteractions.Click) { /* * don't want target.click() here as could trigger an iframe navigation @@ -1184,14 +1214,16 @@ export class Replayer { * removal and addition of .active class (along with void line to trigger repaint) * triggers the 'click' css animation in styles/style.css */ - this.mouse.classList.remove('active'); - void this.mouse.offsetWidth; - this.mouse.classList.add('active'); + this.multiTouchMap[touchId].mouse.classList.remove('active'); + void this.multiTouchMap[touchId].mouse.offsetWidth; + this.multiTouchMap[touchId].mouse.classList.add('active'); } else if (d.type === MouseInteractions.TouchStart) { - void this.mouse.offsetWidth; // needed for the position update of moveAndHover to apply without the .touch-active transition - this.mouse.classList.add('touch-active'); + void this.multiTouchMap[touchId].mouse.offsetWidth; // needed for the position update of moveAndHover to apply without the .touch-active transition + this.multiTouchMap[touchId].mouse.classList.add('touch-active'); } else if (d.type === MouseInteractions.TouchEnd) { - this.mouse.classList.remove('touch-active'); + this.multiTouchMap[touchId].mouse.classList.remove( + 'touch-active', + ); } else { // for MouseDown & MouseUp also invoke default behavior target.dispatchEvent(event); @@ -1200,9 +1232,11 @@ export class Replayer { break; case MouseInteractions.TouchCancel: if (isSync) { - this.touchActive = false; + this.multiTouchMap[touchId].touchActive = false; } else { - this.mouse.classList.remove('touch-active'); + this.multiTouchMap[touchId].mouse.classList.remove( + 'touch-active', + ); } break; default: @@ -2080,6 +2114,7 @@ export class Replayer { id: number, isSync: boolean, debugData: incrementalData, + touchId: number, ) { const target = this.mirror.getNode(id); if (!target) { @@ -2090,15 +2125,15 @@ export class Replayer { const _x = x * base.absoluteScale + base.x; const _y = y * base.absoluteScale + base.y; - this.mouse.style.left = `${_x}px`; - this.mouse.style.top = `${_y}px`; + this.multiTouchMap[touchId].mouse.style.left = `${_x}px`; + this.multiTouchMap[touchId].mouse.style.top = `${_y}px`; if (!isSync) { - this.drawMouseTail({ x: _x, y: _y }); + this.drawMouseTail({ x: _x, y: _y }, touchId); } this.hoverElements(target as Element); } - private drawMouseTail(position: { x: number; y: number }) { + private drawMouseTail(position: { x: number; y: number }, touchId: number) { if (!this.mouseTail) { return; } @@ -2113,7 +2148,7 @@ export class Replayer { return; } const ctx = this.mouseTail.getContext('2d'); - if (!ctx || !this.tailPositions.length) { + if (!ctx || !this.multiTouchMap[touchId].tailPositions.length) { return; } ctx.clearRect(0, 0, this.mouseTail.width, this.mouseTail.height); @@ -2121,15 +2156,22 @@ export class Replayer { ctx.lineWidth = lineWidth; ctx.lineCap = lineCap; ctx.strokeStyle = strokeStyle; - ctx.moveTo(this.tailPositions[0].x, this.tailPositions[0].y); - this.tailPositions.forEach((p) => ctx.lineTo(p.x, p.y)); + ctx.moveTo( + this.multiTouchMap[touchId].tailPositions[0].x, + this.multiTouchMap[touchId].tailPositions[0].y, + ); + this.multiTouchMap[touchId].tailPositions.forEach((p) => + ctx.lineTo(p.x, p.y), + ); ctx.stroke(); }; - this.tailPositions.push(position); + this.multiTouchMap[touchId].tailPositions.push(position); draw(); setTimeout(() => { - this.tailPositions = this.tailPositions.filter((p) => p !== position); + this.multiTouchMap[touchId].tailPositions = this.multiTouchMap[ + touchId + ].tailPositions.filter((p) => p !== position); draw(); }, duration / this.speedService.state.context.timer.speed); } diff --git a/yarn.lock b/yarn.lock index 3fd9a22855..9cd9759f44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5491,15 +5491,10 @@ csso@^4.0.2: dependencies: css-tree "^1.1.2" -cssom@^0.4.4, "cssom@https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz": +cssom@^0.4.4, cssom@^0.5.0, "cssom@https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz": version "0.6.0" resolved "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" -cssom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" - integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== - cssom@~0.3.6: version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -7358,7 +7353,7 @@ fs-extra@^7.0.1, fs-extra@~7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^8.1.0: +fs-extra@^8.0.1, fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -8147,6 +8142,13 @@ ignore-styles@^5.0.1: resolved "https://registry.npmjs.org/ignore-styles/-/ignore-styles-5.0.1.tgz" integrity sha1-tJ7yJ0va/NikiAqWa/440aC/RnE= +ignore-walk@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" + integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== + dependencies: + minimatch "^3.0.4" + ignore-walk@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-5.0.1.tgz#5f199e23e1288f518d90358d461387788a154776" @@ -8278,7 +8280,7 @@ inherits@2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@2.0.0, ini@~2.0.0: +ini@2.0.0, ini@^2.0.0, ini@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== @@ -11142,7 +11144,7 @@ normalize-url@^4.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== -npm-bundled@^1.1.2: +npm-bundled@^1.1.1, npm-bundled@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== @@ -11202,6 +11204,16 @@ npm-packlist@5.1.1: npm-bundled "^1.1.2" npm-normalize-package-bin "^1.0.1" +npm-packlist@^2.1.5: + version "2.2.2" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8" + integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg== + dependencies: + glob "^7.1.6" + ignore-walk "^3.0.3" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + npm-packlist@^7.0.0: version "7.0.4" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-7.0.4.tgz#033bf74110eb74daf2910dc75144411999c5ff32" @@ -15616,6 +15628,20 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yalc@^1.0.0-pre.53: + version "1.0.0-pre.53" + resolved "https://registry.yarnpkg.com/yalc/-/yalc-1.0.0-pre.53.tgz#c51db2bb924a6908f4cb7e82af78f7e5606810bc" + integrity sha512-tpNqBCpTXplnduzw5XC+FF8zNJ9L/UXmvQyyQj7NKrDNavbJtHvzmZplL5ES/RCnjX7JR7W9wz5GVDXVP3dHUQ== + dependencies: + chalk "^4.1.0" + detect-indent "^6.0.0" + fs-extra "^8.0.1" + glob "^7.1.4" + ignore "^5.0.4" + ini "^2.0.0" + npm-packlist "^2.1.5" + yargs "^16.1.1" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -15659,7 +15685,7 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@16.2.0, yargs@^16.2.0: +yargs@16.2.0, yargs@^16.1.1, yargs@^16.2.0: version "16.2.0" resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==