port remaining js components to ts

This commit is contained in:
Labhansh Agrawal 2020-03-18 20:56:46 +05:30 committed by Benjamin Staneck
parent 4a3132b8e7
commit 78ec88d1e8
8 changed files with 170 additions and 83 deletions

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import {SearchBoxProps} from '../hyper';
const searchBoxStyling = { const searchBoxStyling: React.CSSProperties = {
float: 'right', float: 'right',
height: '28px', height: '28px',
backgroundColor: 'white', backgroundColor: 'white',
@ -8,21 +9,22 @@ const searchBoxStyling = {
right: '10px', right: '10px',
top: '25px', top: '25px',
width: '224px', width: '224px',
zIndex: '9999' zIndex: 9999
}; };
const enterKey = 13; const enterKey = 13;
export default class SearchBox extends React.PureComponent { export default class SearchBox extends React.PureComponent<SearchBoxProps> {
constructor(props) { searchTerm: string;
constructor(props: SearchBoxProps) {
super(props); super(props);
this.searchTerm = ''; this.searchTerm = '';
} }
handleChange = event => { handleChange = (event: React.KeyboardEvent<HTMLInputElement>) => {
this.searchTerm = event.target.value; this.searchTerm = event.currentTarget.value;
if (event.keyCode === enterKey) { if (event.keyCode === enterKey) {
this.props.search(event.target.value); this.props.search(event.currentTarget.value);
} }
}; };

View file

@ -1,15 +1,18 @@
import React from 'react'; import React from 'react';
import {Terminal} from 'xterm'; import {Terminal, ITerminalOptions, IDisposable} from 'xterm';
import {FitAddon} from 'xterm-addon-fit'; import {FitAddon} from 'xterm-addon-fit';
import {WebLinksAddon} from 'xterm-addon-web-links'; import {WebLinksAddon} from 'xterm-addon-web-links';
import {SearchAddon} from 'xterm-addon-search'; import {SearchAddon} from 'xterm-addon-search';
import {WebglAddon} from 'xterm-addon-webgl'; import {WebglAddon} from 'xterm-addon-webgl';
import {LigaturesAddon} from 'xterm-addon-ligatures'; import {LigaturesAddon} from 'xterm-addon-ligatures';
import {clipboard} from 'electron'; import {clipboard} from 'electron';
import * as Color from 'color'; import Color from 'color';
import terms from '../terms'; import terms from '../terms';
import processClipboard from '../utils/paste'; import processClipboard from '../utils/paste';
import SearchBox from './searchBox'; import SearchBox from './searchBox';
import ResizeObserver from 'resize-observer-polyfill';
import {TermProps} from '../hyper';
import {ObjectTypedKeys} from '../utils/object';
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform); const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform);
@ -18,7 +21,7 @@ const CURSOR_STYLES = {
BEAM: 'bar', BEAM: 'bar',
UNDERLINE: 'underline', UNDERLINE: 'underline',
BLOCK: 'block' BLOCK: 'block'
}; } as const;
const isWebgl2Supported = (() => { const isWebgl2Supported = (() => {
let isSupported = window.WebGL2RenderingContext ? undefined : false; let isSupported = window.WebGL2RenderingContext ? undefined : false;
@ -32,7 +35,7 @@ const isWebgl2Supported = (() => {
}; };
})(); })();
const getTermOptions = props => { const getTermOptions = (props: TermProps): ITerminalOptions => {
// Set a background color only if it is opaque // Set a background color only if it is opaque
const needTransparency = Color(props.backgroundColor).alpha() < 1; const needTransparency = Color(props.backgroundColor).alpha() < 1;
const backgroundColor = needTransparency ? 'transparent' : props.backgroundColor; const backgroundColor = needTransparency ? 'transparent' : props.backgroundColor;
@ -78,13 +81,23 @@ const getTermOptions = props => {
}; };
}; };
export default class Term extends React.PureComponent { export default class Term extends React.PureComponent<TermProps> {
constructor(props) { termRef: HTMLElement | null;
termWrapperRef: HTMLElement | null;
termOptions: ITerminalOptions;
disposableListeners: IDisposable[];
termDefaultBellSound: string | null;
fitAddon: FitAddon;
searchAddon: SearchAddon;
static rendererTypes: Record<string, string>;
term!: Terminal;
resizeObserver!: ResizeObserver;
resizeTimeout!: NodeJS.Timeout;
constructor(props: TermProps) {
super(props); super(props);
props.ref_(props.uid, this); props.ref_(props.uid, this);
this.termRef = null; this.termRef = null;
this.termWrapperRef = null; this.termWrapperRef = null;
this.termRect = null;
this.termOptions = {}; this.termOptions = {};
this.disposableListeners = []; this.disposableListeners = [];
this.termDefaultBellSound = null; this.termDefaultBellSound = null;
@ -93,7 +106,7 @@ export default class Term extends React.PureComponent {
} }
// The main process shows this in the About dialog // The main process shows this in the About dialog
static reportRenderer(uid, type) { static reportRenderer(uid: string, type: string) {
const rendererTypes = Term.rendererTypes || {}; const rendererTypes = Term.rendererTypes || {};
if (rendererTypes[uid] !== type) { if (rendererTypes[uid] !== type) {
rendererTypes[uid] = type; rendererTypes[uid] = type;
@ -111,14 +124,14 @@ export default class Term extends React.PureComponent {
// The parent element for the terminal is attached and removed manually so // The parent element for the terminal is attached and removed manually so
// that we can preserve it across mounts and unmounts of the component // that we can preserve it across mounts and unmounts of the component
this.termRef = props.term ? props.term.element.parentElement : document.createElement('div'); this.termRef = props.term ? props.term.element!.parentElement! : document.createElement('div');
this.termRef.className = 'term_fit term_term'; this.termRef.className = 'term_fit term_term';
this.termWrapperRef.appendChild(this.termRef); this.termWrapperRef?.appendChild(this.termRef);
if (!props.term) { if (!props.term) {
let needTransparency = Color(props.backgroundColor).alpha() < 1; const needTransparency = Color(props.backgroundColor).alpha() < 1;
let useWebGL = false; const useWebGL = false;
if (props.webGLRenderer) { if (props.webGLRenderer) {
if (needTransparency) { if (needTransparency) {
console.warn( console.warn(
@ -148,8 +161,8 @@ export default class Term extends React.PureComponent {
} }
} else { } else {
// get the cached plugins // get the cached plugins
this.fitAddon = props.fitAddon; this.fitAddon = props.fitAddon!;
this.searchAddon = props.searchAddon; this.searchAddon = props.searchAddon!;
} }
this.fitAddon.fit(); this.fitAddon.fit();
@ -163,9 +176,9 @@ export default class Term extends React.PureComponent {
} }
if (props.onActive) { if (props.onActive) {
this.term.textarea.addEventListener('focus', props.onActive); this.term.textarea?.addEventListener('focus', props.onActive);
this.disposableListeners.push({ this.disposableListeners.push({
dispose: () => this.term.textarea.removeEventListener('focus', this.props.onActive) dispose: () => this.term.textarea?.removeEventListener('focus', this.props.onActive)
}); });
} }
@ -188,14 +201,14 @@ export default class Term extends React.PureComponent {
this.disposableListeners.push( this.disposableListeners.push(
this.term.onCursorMove(() => { this.term.onCursorMove(() => {
const cursorFrame = { const cursorFrame = {
x: this.term.buffer.cursorX * this.term._core._renderService.dimensions.actualCellWidth, x: this.term.buffer.cursorX * (this.term as any)._core._renderService.dimensions.actualCellWidth,
y: this.term.buffer.cursorY * this.term._core._renderService.dimensions.actualCellHeight, y: this.term.buffer.cursorY * (this.term as any)._core._renderService.dimensions.actualCellHeight,
width: this.term._core._renderService.dimensions.actualCellWidth, width: (this.term as any)._core._renderService.dimensions.actualCellWidth,
height: this.term._core._renderService.dimensions.actualCellHeight, height: (this.term as any)._core._renderService.dimensions.actualCellHeight,
col: this.term.buffer.cursorX, col: this.term.buffer.cursorX,
row: this.term.buffer.cursorY row: this.term.buffer.cursorY
}; };
props.onCursorMove(cursorFrame); props.onCursorMove?.(cursorFrame);
}) })
); );
} }
@ -220,18 +233,18 @@ export default class Term extends React.PureComponent {
// intercepting paste event for any necessary processing of // intercepting paste event for any necessary processing of
// clipboard data, if result is falsy, paste event continues // clipboard data, if result is falsy, paste event continues
onWindowPaste = e => { onWindowPaste = (e: any) => {
if (!this.props.isTermActive) return; if (!this.props.isTermActive) return;
const processed = processClipboard(); const processed = processClipboard();
if (processed) { if (processed) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.term._core.handler(processed); (this.term as any)._core.handler(processed);
} }
}; };
onMouseUp = e => { onMouseUp = (e: React.MouseEvent) => {
if (this.props.quickEdit && e.button === 2) { if (this.props.quickEdit && e.button === 2) {
if (this.term.hasSelection()) { if (this.term.hasSelection()) {
clipboard.writeText(this.term.getSelection()); clipboard.writeText(this.term.getSelection());
@ -244,7 +257,7 @@ export default class Term extends React.PureComponent {
} }
}; };
write(data) { write(data: string | Uint8Array) {
this.term.write(data); this.term.write(data);
} }
@ -260,15 +273,15 @@ export default class Term extends React.PureComponent {
this.term.reset(); this.term.reset();
} }
search = searchTerm => { search = (searchTerm = '') => {
this.searchAddon.findNext(searchTerm); this.searchAddon.findNext(searchTerm);
}; };
searchNext = searchTerm => { searchNext = (searchTerm: string) => {
this.searchAddon.findNext(searchTerm); this.searchAddon.findNext(searchTerm);
}; };
searchPrevious = searchTerm => { searchPrevious = (searchTerm: string) => {
this.searchAddon.findPrevious(searchTerm); this.searchAddon.findPrevious(searchTerm);
}; };
@ -276,7 +289,7 @@ export default class Term extends React.PureComponent {
this.props.toggleSearch(); this.props.toggleSearch();
}; };
resize(cols, rows) { resize(cols: number, rows: number) {
this.term.resize(cols, rows); this.term.resize(cols, rows);
} }
@ -291,12 +304,12 @@ export default class Term extends React.PureComponent {
this.fitAddon.fit(); this.fitAddon.fit();
} }
keyboardHandler(e) { keyboardHandler(e: any) {
// Has Mousetrap flagged this event as a command? // Has Mousetrap flagged this event as a command?
return !e.catched; return !e.catched;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: TermProps) {
if (!prevProps.cleared && this.props.cleared) { if (!prevProps.cleared && this.props.cleared) {
this.clear(); this.clear();
} }
@ -305,14 +318,14 @@ export default class Term extends React.PureComponent {
// Use bellSound in nextProps if it exists // Use bellSound in nextProps if it exists
// otherwise use the default sound found in xterm. // otherwise use the default sound found in xterm.
nextTermOptions.bellSound = this.props.bellSound || this.termDefaultBellSound; nextTermOptions.bellSound = this.props.bellSound || this.termDefaultBellSound!;
if (!prevProps.search && this.props.search) { if (!prevProps.search && this.props.search) {
this.search(); this.search();
} }
// Update only options that have changed. // Update only options that have changed.
Object.keys(nextTermOptions) ObjectTypedKeys(nextTermOptions)
.filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option]) .filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option])
.forEach(option => { .forEach(option => {
try { try {
@ -330,8 +343,8 @@ export default class Term extends React.PureComponent {
const shouldUpdateTheme = const shouldUpdateTheme =
!this.termOptions.theme || !this.termOptions.theme ||
nextTermOptions.rendererType !== this.termOptions.rendererType || nextTermOptions.rendererType !== this.termOptions.rendererType ||
Object.keys(nextTermOptions.theme).some( ObjectTypedKeys(nextTermOptions.theme!).some(
option => nextTermOptions.theme[option] !== this.termOptions.theme[option] option => nextTermOptions.theme![option] !== this.termOptions.theme![option]
); );
if (shouldUpdateTheme) { if (shouldUpdateTheme) {
this.term.setOption('theme', nextTermOptions.theme); this.term.setOption('theme', nextTermOptions.theme);
@ -350,11 +363,11 @@ export default class Term extends React.PureComponent {
} }
if (prevProps.rows !== this.props.rows || prevProps.cols !== this.props.cols) { if (prevProps.rows !== this.props.rows || prevProps.cols !== this.props.cols) {
this.resize(this.props.cols, this.props.rows); this.resize(this.props.cols!, this.props.rows!);
} }
} }
onTermWrapperRef = component => { onTermWrapperRef = (component: HTMLElement | null) => {
this.termWrapperRef = component; this.termWrapperRef = component;
if (component) { if (component) {
@ -372,7 +385,7 @@ export default class Term extends React.PureComponent {
componentWillUnmount() { componentWillUnmount() {
terms[this.props.uid] = null; terms[this.props.uid] = null;
this.termWrapperRef.removeChild(this.termRef); this.termWrapperRef?.removeChild(this.termRef!);
this.props.ref_(this.props.uid, null); this.props.ref_(this.props.uid, null);
// to clean up the terminal, we remove the listeners // to clean up the terminal, we remove the listeners

View file

@ -3,62 +3,53 @@ import {decorate, getTermGroupProps} from '../utils/plugins';
import {registerCommandHandlers} from '../command-registry'; import {registerCommandHandlers} from '../command-registry';
import TermGroup_ from './term-group'; import TermGroup_ from './term-group';
import StyleSheet_ from './style-sheet'; import StyleSheet_ from './style-sheet';
import {TermsProps} from '../hyper';
import Term from './term';
import {ObjectTypedKeys} from '../utils/object';
const TermGroup = decorate(TermGroup_, 'TermGroup'); const TermGroup = decorate(TermGroup_, 'TermGroup');
const StyleSheet = decorate(StyleSheet_, 'StyleSheet'); const StyleSheet = decorate(StyleSheet_, 'StyleSheet');
const isMac = /Mac/.test(navigator.userAgent); const isMac = /Mac/.test(navigator.userAgent);
export default class Terms extends React.Component { export default class Terms extends React.Component<TermsProps> {
constructor(props, context) { terms: Record<string, Term>;
registerCommands: (cmds: Record<string, (...args: any[]) => void>) => void;
constructor(props: TermsProps, context: any) {
super(props, context); super(props, context);
this.terms = {}; this.terms = {};
this.bound = new WeakMap();
this.registerCommands = registerCommandHandlers; this.registerCommands = registerCommandHandlers;
props.ref_(this); props.ref_(this);
} }
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps: TermsProps & {children: any}) {
for (const i in nextProps) { return (
if (i === 'write') { ObjectTypedKeys(nextProps).some(i => i !== 'write' && this.props[i] !== nextProps[i]) ||
continue; ObjectTypedKeys(this.props).some(i => i !== 'write' && this.props[i] !== nextProps[i])
} );
if (this.props[i] !== nextProps[i]) {
return true;
}
}
for (const i in this.props) {
if (i === 'write') {
continue;
}
if (this.props[i] !== nextProps[i]) {
return true;
}
}
return false;
} }
onRef = (uid, term) => { onRef = (uid: string, term: Term) => {
if (term) { if (term) {
this.terms[uid] = term; this.terms[uid] = term;
} }
}; };
getTermByUid(uid) { getTermByUid(uid: string) {
return this.terms[uid]; return this.terms[uid];
} }
getActiveTerm() { getActiveTerm() {
return this.getTermByUid(this.props.activeSession); return this.getTermByUid(this.props.activeSession!);
} }
onTerminal(uid, term) { onTerminal(uid: string, term: Term) {
this.terms[uid] = term; this.terms[uid] = term;
} }
componentDidMount() { componentDidMount() {
window.addEventListener('contextmenu', () => { window.addEventListener('contextmenu', () => {
const selection = window.getSelection().toString(); const selection = window.getSelection()!.toString();
const { const {
props: {uid} props: {uid}
} = this.getActiveTerm(); } = this.getActiveTerm();
@ -66,8 +57,8 @@ export default class Terms extends React.Component {
}); });
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: TermsProps) {
for (let uid in prevProps.sessions) { for (const uid in prevProps.sessions) {
if (!this.props.sessions[uid]) { if (!this.props.sessions[uid]) {
this.terms[uid].term.dispose(); this.terms[uid].term.dispose();
delete this.terms[uid]; delete this.terms[uid];

67
lib/hyper.d.ts vendored
View file

@ -26,7 +26,7 @@ export type ITermState = {
}; };
export type cursorShapes = 'BEAM' | 'UNDERLINE' | 'BLOCK'; export type cursorShapes = 'BEAM' | 'UNDERLINE' | 'BLOCK';
import {FontWeight} from 'xterm'; import {FontWeight, Terminal} from 'xterm';
export type uiState = { export type uiState = {
_lastUpdate: number | null; _lastUpdate: number | null;
@ -221,7 +221,7 @@ export type TabProps = {
isActive: boolean; isActive: boolean;
isFirst: boolean; isFirst: boolean;
isLast: boolean; isLast: boolean;
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void; onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onClose: () => void; onClose: () => void;
onSelect: () => void; onSelect: () => void;
text: string; text: string;
@ -237,8 +237,8 @@ export type ITab = {
export type TabsProps = { export type TabsProps = {
tabs: ITab[]; tabs: ITab[];
borderColor: string; borderColor: string;
onChange: () => void; onChange: (uid: string) => void;
onClose: () => void; onClose: (uid: string) => void;
fullScreen: boolean; fullScreen: boolean;
} & extensionProps; } & extensionProps;
@ -312,3 +312,62 @@ export type TermGroupOwnProps = {
import {TermGroupConnectedProps} from './components/term-group'; import {TermGroupConnectedProps} from './components/term-group';
export type TermGroupProps = TermGroupConnectedProps & TermGroupOwnProps; export type TermGroupProps = TermGroupConnectedProps & TermGroupOwnProps;
export type SearchBoxProps = {
search: (searchTerm: string) => void;
next: (searchTerm: string) => void;
prev: (searchTerm: string) => void;
close: () => void;
};
import {FitAddon} from 'xterm-addon-fit';
import {SearchAddon} from 'xterm-addon-search';
export type TermProps = {
backgroundColor: string;
bell: string;
bellSound: string | null;
bellSoundURL: string | null;
borderColor: string;
cleared: boolean;
colors: uiState['colors'];
cols: number | null;
copyOnSelect: boolean;
cursorAccentColor?: string;
cursorBlink: boolean;
cursorColor: string;
cursorShape: cursorShapes;
disableLigatures: boolean;
fitAddon: FitAddon | null;
fontFamily: string;
fontSize: number;
fontSmoothing?: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
foregroundColor: string;
isTermActive: boolean;
letterSpacing: number;
lineHeight: number;
macOptionSelectionMode: string;
modifierKeys: Immutable<{altIsMeta: boolean; cmdIsMeta: boolean}>;
onActive: () => void;
onContextMenu: (selection: any) => void;
onCursorMove?: (cursorFrame: {x: number; y: number; width: number; height: number; col: number; row: number}) => void;
onData: (data: any) => void;
onResize: (cols: number, rows: number) => void;
onTitle: (title: string) => void;
padding: string;
quickEdit: boolean;
rows: number | null;
scrollback: number;
search: boolean;
searchAddon: SearchAddon | null;
selectionColor: string;
term: Terminal | null;
toggleSearch: () => void;
uid: string;
uiFontFamily: string;
url: string | null;
webGLRenderer: boolean;
} & extensionProps & {ref_?: any};
export type Assignable<T, U> = {[k in keyof U]: k extends keyof T ? T[k] : U[k]} & Partial<T>;

View file

@ -7,5 +7,5 @@ import Term from './components/term';
// optimization for the most common action // optimization for the most common action
// within the system // within the system
const terms: Record<string, Term> = {}; const terms: Record<string, Term | null> = {};
export default terms; export default terms;

View file

@ -11,7 +11,19 @@ import React, {PureComponent} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Notification from '../components/notification'; import Notification from '../components/notification';
import notify from './notify'; import notify from './notify';
import {hyperPlugin, IUiReducer, ISessionReducer, ITermGroupReducer, HyperState, HyperDispatch} from '../hyper'; import {
hyperPlugin,
IUiReducer,
ISessionReducer,
ITermGroupReducer,
HyperState,
HyperDispatch,
TabProps,
TabsProps,
TermGroupOwnProps,
TermProps,
Assignable
} from '../hyper';
import {Middleware} from 'redux'; import {Middleware} from 'redux';
import {ObjectTypedKeys} from './object'; import {ObjectTypedKeys} from './object';
@ -402,19 +414,23 @@ function getProps(name: keyof typeof propsDecorators, props: any, ...fnArgs: any
return props_ || props; return props_ || props;
} }
export function getTermGroupProps(uid: string, parentProps: any, props: any) { export function getTermGroupProps<T extends Assignable<TermGroupOwnProps, T>>(
uid: string,
parentProps: any,
props: T
): T {
return getProps('getTermGroupProps', props, uid, parentProps); return getProps('getTermGroupProps', props, uid, parentProps);
} }
export function getTermProps(uid: string, parentProps: any, props: any) { export function getTermProps<T extends Assignable<TermProps, T>>(uid: string, parentProps: any, props: T): T {
return getProps('getTermProps', props, uid, parentProps); return getProps('getTermProps', props, uid, parentProps);
} }
export function getTabsProps(parentProps: any, props: any) { export function getTabsProps<T extends Assignable<TabsProps, T>>(parentProps: any, props: T): T {
return getProps('getTabsProps', props, parentProps); return getProps('getTabsProps', props, parentProps);
} }
export function getTabProps(tab: any, parentProps: any, props: any) { export function getTabProps<T extends Assignable<TabProps, T>>(tab: any, parentProps: any, props: T): T {
return getProps('getTabProps', props, tab, parentProps); return getProps('getTabProps', props, tab, parentProps);
} }

View file

@ -116,6 +116,7 @@
"prettier": "1.19.1", "prettier": "1.19.1",
"proxyquire": "2.1.3", "proxyquire": "2.1.3",
"redux-devtools-extension": "2.13.8", "redux-devtools-extension": "2.13.8",
"resize-observer-polyfill": "1.5.1",
"spectron": "10.0.1", "spectron": "10.0.1",
"style-loader": "1.1.3", "style-loader": "1.1.3",
"terser": "4.6.6", "terser": "4.6.6",

View file

@ -7049,6 +7049,11 @@ reselect@4.0.0:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
resize-observer-polyfill@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"