Skip to content

Instantly share code, notes, and snippets.

@satya164
Created February 16, 2018 09:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save satya164/3d91c91fb72a4d57031ab44ba8f0adaf to your computer and use it in GitHub Desktop.
Save satya164/3d91c91fb72a4d57031ab44ba8f0adaf to your computer and use it in GitHub Desktop.
/* @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