diff --git a/tabby-electron/package.json b/tabby-electron/package.json index a2a6f57b..15b00fc3 100644 --- a/tabby-electron/package.json +++ b/tabby-electron/package.json @@ -16,16 +16,15 @@ ], "author": "Eugene Pankov", "license": "MIT", - "dependencies": { - "hasbin": "^1.2.3" - }, "peerDependencies": { "@angular/core": "^9.1.9", "tabby-local": "*" }, "devDependencies": { "electron-promise-ipc": "^2.2.4", + "ps-node": "^0.1.6", "tmp-promise": "^3.0.2", + "hasbin": "^1.2.3", "winston": "^3.3.3" } } diff --git a/tabby-electron/src/index.ts b/tabby-electron/src/index.ts index 605f0834..11d24763 100644 --- a/tabby-electron/src/index.ts +++ b/tabby-electron/src/index.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core' import { PlatformService, LogService, UpdaterService, DockingService, HostAppService, ThemesService, Platform, AppService, ConfigService, WIN_BUILD_FLUENT_BG_SUPPORTED, isWindowsBuild, HostWindowService, HotkeyProvider, ConfigProvider, FileProvider } from 'tabby-core' import { TerminalColorSchemeProvider } from 'tabby-terminal' import { SFTPContextMenuItemProvider, SSHProfileImporter, AutoPrivateKeyLocator } from 'tabby-ssh' -import { ShellProvider, UACService } from 'tabby-local' +import { PTYInterface, ShellProvider, UACService } from 'tabby-local' import { auditTime } from 'rxjs' import { HyperColorSchemes } from './colorSchemes' @@ -16,11 +16,13 @@ import { ElectronFileProvider } from './services/fileProvider.service' import { ElectronHostAppService } from './services/hostApp.service' import { ElectronService } from './services/electron.service' import { DockMenuService } from './services/dockMenu.service' +import { ElectronUACService } from './services/uac.service' + import { ElectronHotkeyProvider } from './hotkeys' import { ElectronConfigProvider } from './config' import { EditSFTPContextMenu } from './sftpContextMenu' import { OpenSSHImporter, PrivateKeyLocator, StaticFileImporter } from './sshImporters' -import { ElectronUACService } from './services/uac.service' +import { ElectronPTYInterface } from './pty' import { CmderShellProvider } from './shells/cmder' import { Cygwin32ShellProvider } from './shells/cygwin32' @@ -69,6 +71,8 @@ import { VSDevToolsProvider } from './shells/vs' { provide: UACService, useClass: ElectronUACService }, + { provide: PTYInterface, useClass: ElectronPTYInterface }, + // For WindowsDefaultShellProvider PowerShellCoreShellProvider, WSLShellProvider, diff --git a/tabby-electron/src/pty.ts b/tabby-electron/src/pty.ts new file mode 100644 index 00000000..360ac617 --- /dev/null +++ b/tabby-electron/src/pty.ts @@ -0,0 +1,140 @@ +import * as psNode from 'ps-node' +import { ipcRenderer } from 'electron' +import { ChildProcess, PTYInterface, PTYProxy } from 'tabby-local' +import { getWorkingDirectoryFromPID } from 'native-process-working-directory' + +/* eslint-disable block-scoped-var */ + +try { + var macOSNativeProcessList = require('macos-native-processlist') // eslint-disable-line @typescript-eslint/no-var-requires, no-var +} catch { } + +try { + var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var +} catch { } + +export class ElectronPTYInterface extends PTYInterface { + async spawn (...options: any[]): Promise { + const id = ipcRenderer.sendSync('pty:spawn', ...options) + return new ElectronPTYProxy(id) + } + + async restore (id: string): Promise { + if (ipcRenderer.sendSync('pty:exists', id)) { + return new ElectronPTYProxy(id) + } + return null + } +} + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class ElectronPTYProxy extends PTYProxy { + private subscriptions: Map = new Map() + private truePID: Promise + + constructor ( + private id: string, + ) { + super() + this.truePID = new Promise(async (resolve) => { + let pid = await this.getPID() + try { + await new Promise(r => setTimeout(r, 2000)) + + // Retrieve any possible single children now that shell has fully started + let processes = await this.getChildProcessesInternal(pid) + while (pid && processes.length === 1) { + if (!processes[0].pid) { + break + } + pid = processes[0].pid + processes = await this.getChildProcessesInternal(pid) + } + } finally { + resolve(pid) + } + }) + this.truePID = this.truePID.catch(() => this.getPID()) + } + + getID (): string { + return this.id + } + + getTruePID(): Promise { + return this.truePID + } + + async getPID (): Promise { + return ipcRenderer.sendSync('pty:get-pid', this.id) + } + + subscribe (event: string, handler: (..._: any[]) => void): void { + const key = `pty:${this.id}:${event}` + const newHandler = (_event, ...args) => handler(...args) + this.subscriptions.set(key, newHandler) + ipcRenderer.on(key, newHandler) + } + + ackData (length: number): void { + ipcRenderer.send('pty:ack-data', this.id, length) + } + + unsubscribeAll (): void { + for (const k of this.subscriptions.keys()) { + ipcRenderer.off(k, this.subscriptions.get(k)) + } + } + + async resize (columns: number, rows: number): Promise { + ipcRenderer.send('pty:resize', this.id, columns, rows) + } + + async write (data: Buffer): Promise { + ipcRenderer.send('pty:write', this.id, data) + } + + async kill (signal?: string): Promise { + ipcRenderer.send('pty:kill', this.id, signal) + } + + async getChildProcesses (): Promise { + return this.getChildProcessesInternal(await this.getTruePID()) + } + + async getChildProcessesInternal (truePID: number): Promise { + if (process.platform === 'darwin') { + const processes = await macOSNativeProcessList.getProcessList() + return processes.filter(x => x.ppid === truePID).map(p => ({ + pid: p.pid, + ppid: p.ppid, + command: p.name, + })) + } + if (process.platform === 'win32') { + return new Promise(resolve => { + windowsProcessTree.getProcessTree(truePID, tree => { + resolve(tree ? tree.children.map(child => ({ + pid: child.pid, + ppid: tree.pid, + command: child.name, + })) : []) + }) + }) + } + return new Promise((resolve, reject) => { + psNode.lookup({ ppid: truePID }, (err, processes) => { + if (err) { + reject(err) + return + } + resolve(processes as ChildProcess[]) + }) + }) + } + + async getWorkingDirectory (): Promise { + return getWorkingDirectoryFromPID(await this.getTruePID()) + } + +} diff --git a/tabby-electron/yarn.lock b/tabby-electron/yarn.lock index 261aae14..6a0b80d8 100644 --- a/tabby-electron/yarn.lock +++ b/tabby-electron/yarn.lock @@ -93,6 +93,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +connected-domain@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93" + integrity sha512-lHlohUiJxlpunvDag2Y0pO20bnvarMjnrdciZeuJUqRwrf/5JHNhdpiPIr5GQ8IkqrFj5TDMQwcCjblGo1oeuA== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -368,6 +373,13 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= +ps-node@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3" + integrity sha512-w7QJhUTbu70hpDso0YXDRNKCPNuchV8UTUZsAv0m7Qj5g85oHOJfr9drA1EjvK4nQK/bG8P97W4L6PJ3IQLoOA== + dependencies: + table-parser "^0.1.3" + readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -436,6 +448,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +table-parser@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0" + integrity sha512-LCYeuvqqoPII3lzzYaXKbC3Forb+d2u4bNwhk/9FlivuGRxPE28YEWAYcujeSlLLDlMfvy29+WPybFJZFiKMYg== + dependencies: + connected-domain "^1.0.0" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" diff --git a/tabby-local/package.json b/tabby-local/package.json index 4816ab07..d504549e 100644 --- a/tabby-local/package.json +++ b/tabby-local/package.json @@ -19,7 +19,6 @@ "devDependencies": { "ansi-colors": "^4.1.1", "dataurl": "0.1.0", - "ps-node": "^0.1.6", "runes": "^0.4.2" }, "peerDependencies": { diff --git a/tabby-local/src/api.ts b/tabby-local/src/api.ts index 9e022783..5dad1a53 100644 --- a/tabby-local/src/api.ts +++ b/tabby-local/src/api.ts @@ -59,3 +59,22 @@ export abstract class UACService { abstract patchSessionOptionsForUAC (sessionOptions: SessionOptions): SessionOptions } + +export abstract class PTYProxy { + abstract getID (): string + abstract getPID (): Promise + abstract resize (columns: number, rows: number): Promise + abstract write (data: Buffer): Promise + abstract kill (signal?: string): Promise + abstract ackData (length: number): void + abstract subscribe (event: string, handler: (..._: any[]) => void): void + abstract unsubscribeAll (): void + abstract getChildProcesses (): Promise + abstract getTruePID (): Promise + abstract getWorkingDirectory (): Promise +} + +export abstract class PTYInterface { + abstract spawn (...options: any[]): Promise + abstract restore (id: string): Promise +} diff --git a/tabby-local/src/components/terminalTab.component.ts b/tabby-local/src/components/terminalTab.component.ts index 5b1a78d9..325264d8 100644 --- a/tabby-local/src/components/terminalTab.component.ts +++ b/tabby-local/src/components/terminalTab.component.ts @@ -51,7 +51,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent protected onFrontendReady (): void { this.initializeSession(this.size.columns, this.size.rows) - this.savedStateIsLive = this.profile.options.restoreFromPTYID === this.session?.getPTYID() + this.savedStateIsLive = this.profile.options.restoreFromPTYID === this.session?.getID() super.onFrontendReady() } @@ -82,7 +82,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent options: { ...this.profile.options, cwd: cwd ?? this.profile.options.cwd, - restoreFromPTYID: options?.includeState && this.session?.getPTYID(), + restoreFromPTYID: options?.includeState && this.session?.getID(), }, }, savedState: options?.includeState && this.frontend?.saveState(), diff --git a/tabby-local/src/session.ts b/tabby-local/src/session.ts index 5757f88c..61b27108 100644 --- a/tabby-local/src/session.ts +++ b/tabby-local/src/session.ts @@ -1,87 +1,12 @@ -import * as psNode from 'ps-node' import * as fs from 'mz/fs' import * as fsSync from 'fs' import { Injector } from '@angular/core' import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild, Platform, BootstrapData, BOOTSTRAP_DATA, LogService } from 'tabby-core' import { BaseSession } from 'tabby-terminal' -import { ipcRenderer } from 'electron' -import { getWorkingDirectoryFromPID } from 'native-process-working-directory' -import { SessionOptions, ChildProcess } from './api' - -/* eslint-disable block-scoped-var */ - -try { - var macOSNativeProcessList = require('macos-native-processlist') // eslint-disable-line @typescript-eslint/no-var-requires, no-var -} catch { } - -try { - var windowsProcessTree = require('windows-process-tree') // eslint-disable-line @typescript-eslint/no-var-requires, no-var -} catch { } +import { SessionOptions, ChildProcess, PTYInterface, PTYProxy } from './api' const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class PTYProxy { - private id: string - private subscriptions: Map = new Map() - - static spawn (...options: any[]): PTYProxy { - return new PTYProxy(null, ...options) - } - - static restore (id: string): PTYProxy|null { - if (ipcRenderer.sendSync('pty:exists', id)) { - return new PTYProxy(id) - } - return null - } - - private constructor (id: string|null, ...options: any[]) { - if (id) { - this.id = id - } else { - this.id = ipcRenderer.sendSync('pty:spawn', ...options) - } - } - - getPTYID (): string { - return this.id - } - - getPID (): number { - return ipcRenderer.sendSync('pty:get-pid', this.id) - } - - subscribe (event: string, handler: (..._: any[]) => void): void { - const key = `pty:${this.id}:${event}` - const newHandler = (_event, ...args) => handler(...args) - this.subscriptions.set(key, newHandler) - ipcRenderer.on(key, newHandler) - } - - ackData (length: number): void { - ipcRenderer.send('pty:ack-data', this.id, length) - } - - unsubscribeAll (): void { - for (const k of this.subscriptions.keys()) { - ipcRenderer.off(k, this.subscriptions.get(k)) - } - } - - resize (columns: number, rows: number): void { - ipcRenderer.send('pty:resize', this.id, columns, rows) - } - - write (data: Buffer): void { - ipcRenderer.send('pty:write', this.id, data) - } - - kill (signal?: string): void { - ipcRenderer.send('pty:kill', this.id, signal) - } -} - function mergeEnv (...envs) { const result = {} const keyMap = {} @@ -121,19 +46,23 @@ export class Session extends BaseSession { private config: ConfigService private hostApp: HostAppService private bootstrapData: BootstrapData + private ptyInterface: PTYInterface - constructor (injector: Injector) { + constructor ( + injector: Injector, + ) { super(injector.get(LogService).create('local')) this.config = injector.get(ConfigService) this.hostApp = injector.get(HostAppService) + this.ptyInterface = injector.get(PTYInterface) this.bootstrapData = injector.get(BOOTSTRAP_DATA) } - start (options: SessionOptions): void { + async start (options: SessionOptions): Promise { let pty: PTYProxy|null = null if (options.restoreFromPTYID) { - pty = PTYProxy.restore(options.restoreFromPTYID) + pty = await this.ptyInterface.restore(options.restoreFromPTYID) options.restoreFromPTYID = undefined } @@ -175,7 +104,7 @@ export class Session extends BaseSession { cwd = undefined } - pty = PTYProxy.spawn(options.command, options.args ?? [], { + pty = await this.ptyInterface.spawn(options.command, options.args ?? [], { name: 'xterm-256color', cols: options.width ?? 80, rows: options.height ?? 30, @@ -191,17 +120,9 @@ export class Session extends BaseSession { this.pty = pty - this.truePID = this.pty.getPID() - - setTimeout(async () => { - // Retrieve any possible single children now that shell has fully started - let processes = await this.getChildProcesses() - while (processes.length === 1) { - this.truePID = processes[0].pid - processes = await this.getChildProcesses() - } + pty.getTruePID().then(async () => { this.initialCWD = await this.getWorkingDirectory() - }, 2000) + }) this.open = true @@ -236,8 +157,8 @@ export class Session extends BaseSession { this.destroyed$.subscribe(() => this.pty!.unsubscribeAll()) } - getPTYID (): string|null { - return this.pty?.getPTYID() ?? null + getID (): string|null { + return this.pty?.getID() ?? null } resize (columns: number, rows: number): void { @@ -258,37 +179,7 @@ export class Session extends BaseSession { } async getChildProcesses (): Promise { - if (!this.truePID) { - return [] - } - if (this.hostApp.platform === Platform.macOS) { - const processes = await macOSNativeProcessList.getProcessList() - return processes.filter(x => x.ppid === this.truePID).map(p => ({ - pid: p.pid, - ppid: p.ppid, - command: p.name, - })) - } - if (this.hostApp.platform === Platform.Windows) { - return new Promise(resolve => { - windowsProcessTree.getProcessTree(this.truePID, tree => { - resolve(tree ? tree.children.map(child => ({ - pid: child.pid, - ppid: tree.pid, - command: child.name, - })) : []) - }) - }) - } - return new Promise((resolve, reject) => { - psNode.lookup({ ppid: this.truePID }, (err, processes) => { - if (err) { - reject(err) - return - } - resolve(processes as ChildProcess[]) - }) - }) + return this.pty?.getChildProcesses() ?? [] } async gracefullyKillProcess (): Promise { @@ -297,9 +188,9 @@ export class Session extends BaseSession { } else { await new Promise((resolve) => { this.kill('SIGTERM') - setTimeout(() => { + setTimeout(async () => { try { - process.kill(this.pty!.getPID(), 0) + process.kill(await this.pty!.getPID(), 0) // still alive this.kill('SIGKILL') resolve() @@ -312,19 +203,16 @@ export class Session extends BaseSession { } supportsWorkingDirectory (): boolean { - return !!(this.truePID ?? this.reportedCWD ?? this.guessedCWD) + return !!(this.initialCWD ?? this.reportedCWD ?? this.guessedCWD) } async getWorkingDirectory (): Promise { if (this.reportedCWD) { return this.reportedCWD } - if (!this.truePID) { - return null - } let cwd: string|null = null try { - cwd = getWorkingDirectoryFromPID(this.truePID) + cwd = await this.pty?.getWorkingDirectory() ?? null } catch (exc) { console.info('Could not read working directory:', exc) } diff --git a/tabby-local/yarn.lock b/tabby-local/yarn.lock index 6340c69f..70b6b19f 100644 --- a/tabby-local/yarn.lock +++ b/tabby-local/yarn.lock @@ -7,31 +7,12 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -connected-domain@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93" - integrity sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM= - dataurl@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dataurl/-/dataurl-0.1.0.tgz#1f4734feddec05ffe445747978d86759c4b33199" integrity sha1-H0c0/t3sBf/kRXR5eNhnWcSzMZk= -ps-node@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3" - integrity sha1-mvZ6mdex0BMuUaUDCZ04qNKs4sM= - dependencies: - table-parser "^0.1.3" - runes@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355" integrity sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg== - -table-parser@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0" - integrity sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A= - dependencies: - connected-domain "^1.0.0" diff --git a/tabby-terminal/package.json b/tabby-terminal/package.json index 1bedc65f..379d3b3b 100644 --- a/tabby-terminal/package.json +++ b/tabby-terminal/package.json @@ -26,7 +26,6 @@ "hexer": "^1.5.0", "ngx-colors": "^3.4.0", "patch-package": "^6.5.0", - "ps-node": "^0.1.6", "runes": "^0.4.2", "xterm": "^5.1.0", "xterm-addon-canvas": "^0.3.0", diff --git a/tabby-terminal/src/session.ts b/tabby-terminal/src/session.ts index 1a842bca..1770a50c 100644 --- a/tabby-terminal/src/session.ts +++ b/tabby-terminal/src/session.ts @@ -10,7 +10,6 @@ import { SessionMiddlewareStack } from './api/middleware' */ export abstract class BaseSession { open: boolean - truePID?: number readonly oscProcessor = new OSCProcessor() readonly middleware = new SessionMiddlewareStack() protected output = new Subject() @@ -85,7 +84,7 @@ export abstract class BaseSession { this.binaryOutput.complete() } - abstract start (options: unknown): void + abstract start (options: unknown): Promise abstract resize (columns: number, rows: number): void abstract write (data: Buffer): void abstract kill (signal?: string): void diff --git a/tabby-terminal/yarn.lock b/tabby-terminal/yarn.lock index 8df4d50a..a8ac6433 100644 --- a/tabby-terminal/yarn.lock +++ b/tabby-terminal/yarn.lock @@ -89,11 +89,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -connected-domain@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/connected-domain/-/connected-domain-1.0.0.tgz#bfe77238c74be453a79f0cb6058deeb4f2358e93" - integrity sha1-v+dyOMdL5FOnnwy2BY3utPI1jpM= - crc-32@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" @@ -382,13 +377,6 @@ promise-stream-reader@^1.0.1: resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649" integrity sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg== -ps-node@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/ps-node/-/ps-node-0.1.6.tgz#9af67a99d7b1d0132e51a503099d38a8d2ace2c3" - integrity sha1-mvZ6mdex0BMuUaUDCZ04qNKs4sM= - dependencies: - table-parser "^0.1.3" - rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -430,13 +418,6 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -table-parser@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0" - integrity sha1-BEHPzhallIFoTCfRtaZ/8VpDx7A= - dependencies: - connected-domain "^1.0.0" - tiny-inflate@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"