-
-
Save satya164/3d91c91fb72a4d57031ab44ba8f0adaf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* @flow */ | |
import * as React from 'react'; | |
import debounce from 'lodash/debounce'; | |
import pickBy from 'lodash/pickBy'; | |
import flatMap from 'lodash/flatMap'; | |
import JSON5 from 'json5'; | |
import { isModulePreloaded } from 'snack-sdk'; | |
import Toast from './shared/Toast'; | |
import getSnackURLFromEmbed from '../utils/getSnackURLFromEmbed'; | |
import findDependencies from '../utils/findDependencies'; | |
import { isPackageJson } from '../utils/fileUtilities'; | |
import type { SDKVersion } from '../configs/sdk'; | |
import type { FileSystemEntry, TextFileEntry } from '../types'; | |
import { keybindText } from './KeybindingsManager'; | |
type Props = {| | |
sdkVersion: SDKVersion, | |
fileEntries: FileSystemEntry[], | |
dependencies: { [name: string]: string }, | |
installModuleAsync: (name: string, version?: string) => Promise<string>, | |
removeModuleAsync: (name: string) => Promise<string>, | |
shouldInstallAllModules: boolean, | |
resetShouldInstall: any, | |
onOpenFullview?: () => void, | |
sessionID?: string, | |
|}; | |
type State = {| | |
dependencies: { | |
[name: string]: { | |
status: | |
| 'added' // module is newly added | |
| 'installing' // module is currently installing | |
| 'error' // an error occurred while installing the module | |
| 'denied', // user denied installing the module | |
origin?: string, // file path where we found the dependency initially | |
}, | |
}, | |
|}; | |
export default class Dependencymanager extends React.Component<Props, State> { | |
state = { | |
dependencies: {}, | |
}; | |
componentDidMount() { | |
this._syncPackageDependencies( | |
this.props.fileEntries.find(e => isPackageJson(e.item.path)), | |
this.props.fileEntries | |
); | |
} | |
async componentWillReceiveProps(nextProps: Props) { | |
const packageJson: ?TextFileEntry = this.props.fileEntries.find(e => | |
isPackageJson(e.item.path) | |
); | |
const nextPackageJson: ?TextFileEntry = nextProps.fileEntries.find(e => | |
isPackageJson(e.item.path) | |
); | |
if (nextProps.shouldInstallAllModules !== this.props.shouldInstallAllModules) { | |
const { dependencies } = JSON5.parse( | |
// $FlowIgnore | |
this.props.fileEntries.find(e => isPackageJson(e.item.path)).item.content | |
); | |
await Promise.all( | |
Object.keys(this.state.dependencies) | |
.filter(name => { | |
const status = this.state.dependencies[name].status; | |
return status === 'added' || status === 'error'; | |
}) | |
.map(name => this._handleInstallDependency(name, dependencies[name])) | |
); | |
this.props.resetShouldInstall(); | |
} | |
if ( | |
packageJson && | |
nextPackageJson && | |
packageJson.item.content !== nextPackageJson.item.content | |
) { | |
this._syncPackageDependencies(nextPackageJson, nextProps.fileEntries); | |
} else { | |
if (this.props.sdkVersion !== nextProps.sdkVersion) { | |
this._handleDependencies(nextProps.fileEntries.filter(entry => this._isJSFile(entry))); | |
} else if (this.props.fileEntries !== nextProps.fileEntries) { | |
this._handleDependencies( | |
// Find dependencies for new or changed files | |
nextProps.fileEntries.filter( | |
entry => | |
// If file doesn't exist in current entry list, then it's new or changed | |
this._isJSFile(entry) && !this.props.fileEntries.includes(entry) | |
) | |
); | |
} | |
} | |
} | |
_isJSFile = entry => entry && !entry.item.asset && entry.item.path.endsWith('.js'); | |
_syncPackageDependenciesNotDebounced = async (packageJson: ?TextFileEntry, fileEntries) => { | |
if (!packageJson) { | |
return; | |
} | |
let dependencies; | |
try { | |
dependencies = JSON5.parse(packageJson.item.content).dependencies; | |
} catch (e) { | |
return; | |
} | |
// Uninstall removed dependencies | |
Object.keys(this.props.dependencies) | |
.filter(name => !dependencies[name]) | |
.forEach(name => this.props.removeModuleAsync(name)); | |
// Install new dependencies | |
await Promise.all( | |
Object.keys(dependencies) | |
.filter( | |
name => !(dependencies[name] === this.props.dependencies[name] || isModulePreloaded(name)) | |
) | |
.map(name => this._handleInstallDependency(name, dependencies[name])) | |
); | |
this._handleDependencies(fileEntries.filter(entry => this._isJSFile(entry))); | |
}; | |
_syncPackageDependencies = debounce(this._syncPackageDependenciesNotDebounced, 1000); | |
_handleDependenciesNotDebounced = fileEntries => | |
this.setState((state, props) => { | |
const origins = fileEntries.map(entry => entry.item.path); | |
// Filter out dependencies from current file | |
// This makes sure that we don't show dependencies that are no longer mentioned | |
const dependencies = pickBy(state.dependencies, value => !origins.includes(value.origin)); | |
try { | |
return { | |
dependencies: { | |
...dependencies, | |
...flatMap( | |
fileEntries, | |
(entry: FileSystemEntry): Array<{ name: string, origin: string }> => | |
// Get the list of dependencies the file | |
typeof entry.item.content === 'string' | |
? findDependencies(entry.item.content).map(name => ({ | |
name, | |
origin: entry.item.path, | |
})) | |
: [] | |
) | |
.filter( | |
({ name }) => | |
// If the dependency is already in props, or is in progress, don't add them | |
!(dependencies[name] || props.dependencies[name] || isModulePreloaded(name)) | |
) | |
// Find new dependencies and schedule them to install | |
.reduce((acc, { name, origin }) => { | |
// If the dependency had a status before, preserve it | |
// This makes sure not to add deps we denied earlier and were not removed from the file | |
acc[name] = state.dependencies[name] || { status: 'added', origin }; | |
return acc; | |
}, {}), | |
}, | |
}; | |
} catch (e) { | |
return state; | |
} | |
}); | |
_handleDependencies = debounce(this._handleDependenciesNotDebounced, 1000); | |
_handleInstallDependency = async (name: string, version?: string) => { | |
this.setState(state => ({ | |
dependencies: { | |
...state.dependencies, | |
[name]: { | |
...state.dependencies[name], | |
status: 'installing', | |
}, | |
}, | |
})); | |
try { | |
await this.props.installModuleAsync(name, version); | |
this.setState(state => ({ | |
dependencies: pickBy(state.dependencies, (value, key: string) => key !== name), | |
})); | |
} catch (e) { | |
this.setState(state => ({ | |
dependencies: { | |
...state.dependencies, | |
[name]: { | |
...state.dependencies[name], | |
status: 'error', | |
}, | |
}, | |
})); | |
} | |
}; | |
_handleDenyInstalling = (name: string) => | |
this.setState(state => ({ | |
dependencies: { | |
...state.dependencies, | |
[name]: { | |
...state.dependencies[name], | |
status: 'denied', | |
}, | |
}, | |
})); | |
_handleDismissDependencyError = (name: string) => | |
this.setState(state => ({ | |
dependencies: pickBy(state.dependencies, (value, key: string) => name !== key), | |
})); | |
_handleOpenFullEditor = () => { | |
this.props.onOpenFullview(); | |
const link = document.createElement('a'); | |
link.target = '_blank'; | |
link.href = getSnackURLFromEmbed(this.props.sessionID); | |
link.click(); | |
}; | |
render() { | |
const cmdEnter = keybindText({ meta: true, code: 13 }, true); | |
const dependencies = Object.keys(this.state.dependencies).filter(name => { | |
const status = this.state.dependencies[name].status; | |
return status === 'added' || status === 'error'; | |
}); | |
return ( | |
<div> | |
{dependencies.length && this.props.onOpenFullview ? ( | |
<Toast | |
key={name} | |
persistent | |
label={<span>Open full editor to install modules</span>} | |
actions={[ | |
{ | |
label: `Open`, | |
action: this._handleOpenFullEditor, | |
}, | |
]} | |
/> | |
) : ( | |
dependencies.map( | |
name => | |
this.state.dependencies[name].status === 'error' ? ( | |
<Toast | |
key={name} | |
persistent | |
type="error" | |
label={ | |
<span> | |
An error occured when installing <code>{name}</code> | |
</span> | |
} | |
actions={[ | |
{ | |
label: `Retry ${cmdEnter}`, | |
action: () => this._handleInstallDependency(name), | |
}, | |
{ label: 'Dismiss' }, | |
]} | |
onDismiss={() => this._handleDismissDependencyError(name)} | |
/> | |
) : ( | |
<Toast | |
key={name} | |
persistent | |
label={ | |
<span> | |
Install <code>{name}</code>? | |
</span> | |
} | |
actions={[ | |
{ | |
label: `Install ${cmdEnter}`, | |
action: () => this._handleInstallDependency(name), | |
}, | |
{ label: 'Cancel' }, | |
]} | |
onDismiss={() => this._handleDenyInstalling(name)} | |
/> | |
) | |
) | |
)} | |
</div> | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment