mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 14:19:58 +00:00
Add new multipart response viewer (#590)
This commit is contained in:
parent
e05377b513
commit
2547e57dad
@ -193,13 +193,13 @@ export function getAuthTypeName (authType, useLong = false) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getContentTypeFromHeaders (headers) {
|
||||
export function getContentTypeFromHeaders (headers, defaultValue = null) {
|
||||
if (!Array.isArray(headers)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const header = headers.find(({name}) => name.toLowerCase() === 'content-type');
|
||||
return header ? header.value : null;
|
||||
return header ? header.value : defaultValue;
|
||||
}
|
||||
|
||||
export const RESPONSE_CODE_REASONS = {
|
||||
|
52
app/ui/components/modals/wrapper-modal.js
Normal file
52
app/ui/components/modals/wrapper-modal.js
Normal file
@ -0,0 +1,52 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import Modal from '../base/modal';
|
||||
import ModalBody from '../base/modal-body';
|
||||
import ModalHeader from '../base/modal-header';
|
||||
|
||||
type Props = {};
|
||||
type State = {
|
||||
title: string,
|
||||
body: React.Node
|
||||
}
|
||||
|
||||
@autobind
|
||||
class WrapperModal extends React.PureComponent<Props, State> {
|
||||
modal: ?Modal;
|
||||
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
title: '',
|
||||
body: null
|
||||
};
|
||||
}
|
||||
|
||||
_setModalRef (m: ?Modal) {
|
||||
this.modal = m;
|
||||
}
|
||||
|
||||
show (options: Object = {}) {
|
||||
const {title, body} = options;
|
||||
this.setState({title, body});
|
||||
|
||||
this.modal && this.modal.show();
|
||||
}
|
||||
|
||||
render () {
|
||||
const {title, body} = this.state;
|
||||
|
||||
return (
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
|
||||
<ModalBody className="wide pad">
|
||||
{body}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WrapperModal;
|
@ -316,13 +316,12 @@ class RequestPane extends React.PureComponent<Props> {
|
||||
</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="react-tabs__tab-panel editor-wrapper">
|
||||
<TabPanel key={uniqueKey} className="react-tabs__tab-panel editor-wrapper">
|
||||
<BodyEditor
|
||||
handleUpdateRequestMimeType={updateRequestMimeType}
|
||||
handleRender={handleRender}
|
||||
handleGetRenderContext={handleGetRenderContext}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
key={uniqueKey}
|
||||
request={request}
|
||||
workspace={workspace}
|
||||
environmentId={environmentId}
|
||||
@ -337,9 +336,8 @@ class RequestPane extends React.PureComponent<Props> {
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel scrollable-container">
|
||||
<div className="scrollable">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<ErrorBoundary key={uniqueKey} errorClassName="font-error pad text-center">
|
||||
<AuthWrapper
|
||||
key={uniqueKey}
|
||||
oAuth2Token={oAuth2Token}
|
||||
showPasswords={showPasswords}
|
||||
request={request}
|
||||
@ -356,10 +354,9 @@ class RequestPane extends React.PureComponent<Props> {
|
||||
<div className="pad pad-bottom-sm query-editor__preview">
|
||||
<label className="label--small no-pad-top">Url Preview</label>
|
||||
<code className="txt-sm block faint">
|
||||
<ErrorBoundary
|
||||
<ErrorBoundary key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center">
|
||||
<RenderedQueryString
|
||||
key={uniqueKey}
|
||||
handleRender={handleRender}
|
||||
request={request}
|
||||
/>
|
||||
@ -368,11 +365,10 @@ class RequestPane extends React.PureComponent<Props> {
|
||||
</div>
|
||||
<div className="scrollable-container">
|
||||
<div className="scrollable">
|
||||
<ErrorBoundary
|
||||
<ErrorBoundary key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center">
|
||||
<KeyValueEditor
|
||||
sortable
|
||||
key={uniqueKey}
|
||||
namePlaceholder="name"
|
||||
valuePlaceholder="value"
|
||||
onToggleDisable={this._trackQueryToggle}
|
||||
@ -396,9 +392,8 @@ class RequestPane extends React.PureComponent<Props> {
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel header-editor">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<ErrorBoundary key={uniqueKey} errorClassName="font-error pad text-center">
|
||||
<RequestHeadersEditor
|
||||
key={uniqueKey}
|
||||
headers={request.headers}
|
||||
handleRender={handleRender}
|
||||
handleGetRenderContext={handleGetRenderContext}
|
||||
|
@ -279,9 +279,8 @@ class ResponsePane extends React.PureComponent<Props> {
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="react-tabs__tab-panel">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseViewer
|
||||
key={response._id}
|
||||
// Send larger one because legacy responses have bytesContent === -1
|
||||
responseId={response._id}
|
||||
bytes={Math.max(response.bytesContent, response.bytesRead)}
|
||||
@ -303,32 +302,27 @@ class ResponsePane extends React.PureComponent<Props> {
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel scrollable-container">
|
||||
<div className="scrollable pad">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<ResponseHeadersViewer
|
||||
key={response._id}
|
||||
headers={response.headers}
|
||||
/>
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseHeadersViewer headers={response.headers} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel scrollable-container">
|
||||
<div className="scrollable pad">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseCookiesViewer
|
||||
handleShowRequestSettings={handleShowRequestSettings}
|
||||
cookiesSent={response.settingSendCookies}
|
||||
cookiesStored={response.settingStoreCookies}
|
||||
showCookiesModal={showCookiesModal}
|
||||
key={response._id}
|
||||
headers={cookieHeaders}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseTimelineViewer
|
||||
key={response._id}
|
||||
timeline={response.timeline || []}
|
||||
editorLineWrapping={editorLineWrapping}
|
||||
editorFontSize={editorFontSize}
|
||||
|
@ -1,14 +1,17 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import CopyButton from '../base/copy-button';
|
||||
import type {ResponseHeader} from '../../../models/response';
|
||||
|
||||
class ResponseHeadersViewer extends PureComponent {
|
||||
type Props = {
|
||||
headers: Array<ResponseHeader>
|
||||
};
|
||||
|
||||
class ResponseHeadersViewer extends React.PureComponent<Props> {
|
||||
render () {
|
||||
const {headers} = this.props;
|
||||
|
||||
const headersString = headers.map(
|
||||
h => `${h.name}: ${h.value}`
|
||||
).join('\n');
|
||||
const headersString = headers.map(h => `${h.name}: ${h.value}`).join('\n');
|
||||
|
||||
return [
|
||||
<table key='table' className="table--fancy table--striped">
|
||||
@ -37,8 +40,4 @@ class ResponseHeadersViewer extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
ResponseHeadersViewer.propTypes = {
|
||||
headers: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default ResponseHeadersViewer;
|
||||
|
293
app/ui/components/viewers/response-multipart.js
Normal file
293
app/ui/components/viewers/response-multipart.js
Normal file
@ -0,0 +1,293 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as electron from 'electron';
|
||||
import mimes from 'mime-types';
|
||||
import fs from 'fs';
|
||||
import moment from 'moment';
|
||||
import path from 'path';
|
||||
import {PassThrough} from 'stream';
|
||||
import multiparty from 'multiparty';
|
||||
import autobind from 'autobind-decorator';
|
||||
import ResponseViewer from './response-viewer';
|
||||
import {getContentTypeFromHeaders, PREVIEW_MODE_FRIENDLY} from '../../../common/constants';
|
||||
import type {ResponseHeader} from '../../../models/response';
|
||||
import {Dropdown, DropdownButton, DropdownItem} from '../base/dropdown/index';
|
||||
import {trackEvent} from '../../../analytics/index';
|
||||
import WrapperModal from '../modals/wrapper-modal';
|
||||
import {showModal} from '../modals/index';
|
||||
import ResponseHeadersViewer from './response-headers-viewer';
|
||||
|
||||
type Part = {
|
||||
name: string,
|
||||
bytes: number,
|
||||
value: Buffer,
|
||||
filename: string | null,
|
||||
headers: Array<ResponseHeader>,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
responseId: string,
|
||||
bodyBuffer: Buffer | null,
|
||||
contentType: string,
|
||||
filter: string,
|
||||
filterHistory: Array<string>,
|
||||
editorFontSize: number,
|
||||
editorIndentSize: number,
|
||||
editorKeyMap: string,
|
||||
editorLineWrapping: boolean,
|
||||
url: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
activePart: number,
|
||||
parts: Array<Part>,
|
||||
error: string | null
|
||||
};
|
||||
|
||||
@autobind
|
||||
class ResponseMultipart extends React.PureComponent<Props, State> {
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activePart: -1,
|
||||
parts: [],
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._setParts();
|
||||
}
|
||||
|
||||
async _setParts () {
|
||||
try {
|
||||
const parts = await this._getParts();
|
||||
this.setState({parts, activePart: 0, error: null});
|
||||
} catch (err) {
|
||||
this.setState({error: err.message});
|
||||
}
|
||||
}
|
||||
|
||||
_describePart (part: Part) {
|
||||
const segments = [part.name];
|
||||
if (part.filename) {
|
||||
segments.push(`(${part.filename})`);
|
||||
}
|
||||
|
||||
return segments.join(' ');
|
||||
}
|
||||
|
||||
async _handleSelectPart (index: number) {
|
||||
this.setState({activePart: index});
|
||||
}
|
||||
|
||||
_getBody () {
|
||||
const {parts, activePart} = this.state;
|
||||
const part = parts[activePart];
|
||||
|
||||
if (!part) {
|
||||
return new Buffer('');
|
||||
}
|
||||
|
||||
return part.value;
|
||||
}
|
||||
|
||||
_handleViewHeaders () {
|
||||
const {parts, activePart} = this.state;
|
||||
const part = parts[activePart];
|
||||
|
||||
if (!part) {
|
||||
return;
|
||||
}
|
||||
|
||||
showModal(WrapperModal, {
|
||||
title: <span>Headers for <code>{part.name}</code></span>,
|
||||
body: <ResponseHeadersViewer
|
||||
headers={[...part.headers, ...part.headers, ...part.headers, ...part.headers]}/>
|
||||
});
|
||||
}
|
||||
|
||||
_handleSaveAsFile () {
|
||||
const {parts, activePart} = this.state;
|
||||
const part = parts[activePart];
|
||||
|
||||
if (!part) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = getContentTypeFromHeaders(part.headers, 'text/plain');
|
||||
|
||||
const extension = mimes.extension(contentType) || '.txt';
|
||||
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
|
||||
const dir = lastDir || electron.remote.app.getPath('desktop');
|
||||
const date = moment().format('YYYY-MM-DD');
|
||||
const filename = part.filename || `${part.name}_${date}`;
|
||||
const options = {
|
||||
title: 'Save as File',
|
||||
buttonLabel: 'Save',
|
||||
defaultPath: path.join(dir, filename),
|
||||
filters: [{
|
||||
name: 'Download', extensions: [extension]
|
||||
}]
|
||||
};
|
||||
|
||||
electron.remote.dialog.showSaveDialog(options, outputPath => {
|
||||
if (!outputPath) {
|
||||
trackEvent('Response', 'Multipart Save Cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remember last exported path
|
||||
window.localStorage.setItem('insomnia.lastExportPath', path.dirname(filename));
|
||||
|
||||
// Save the file
|
||||
fs.writeFile(outputPath, part.value, err => {
|
||||
if (err) {
|
||||
console.warn('Failed to save multipart to file', err);
|
||||
trackEvent('Response', 'Multipart Save Failure');
|
||||
} else {
|
||||
trackEvent('Response', 'Multipart Save Success');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getParts (): Promise<Array<Part>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const {bodyBuffer, contentType} = this.props;
|
||||
const parts = [];
|
||||
|
||||
if (!bodyBuffer) {
|
||||
return resolve(parts);
|
||||
}
|
||||
|
||||
const fakeReq = new PassThrough();
|
||||
(fakeReq: Object).headers = {
|
||||
'content-type': contentType
|
||||
};
|
||||
|
||||
const form = new multiparty.Form();
|
||||
form.on('part', part => {
|
||||
const dataBuffers = [];
|
||||
part.on('data', data => {
|
||||
dataBuffers.push(data);
|
||||
});
|
||||
|
||||
part.on('error', err => {
|
||||
reject(new Error(`Failed to parse part: ${err.message}`));
|
||||
});
|
||||
|
||||
part.on('end', () => {
|
||||
parts.push({
|
||||
value: Buffer.concat(dataBuffers),
|
||||
name: part.name,
|
||||
filename: part.filename || null,
|
||||
bytes: part.byteCount,
|
||||
headers: Object.keys(part.headers).map(name => ({name, value: part.headers[name]}))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
form.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
form.on('close', () => {
|
||||
resolve(parts);
|
||||
});
|
||||
|
||||
form.parse(fakeReq);
|
||||
|
||||
fakeReq.write(bodyBuffer);
|
||||
fakeReq.end();
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
responseId,
|
||||
filter,
|
||||
filterHistory,
|
||||
url,
|
||||
editorFontSize,
|
||||
editorIndentSize,
|
||||
editorKeyMap,
|
||||
editorLineWrapping
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
activePart,
|
||||
parts,
|
||||
error
|
||||
} = this.state;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="pad monospace" style={{fontSize: editorFontSize}}>
|
||||
Failed to parse multipart response: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedPart = parts[activePart];
|
||||
|
||||
return (
|
||||
<div className="pad-sm tall wide"
|
||||
style={{display: 'grid', gridTemplateRows: 'auto minmax(0, 1fr)'}}>
|
||||
<div className="pad-bottom-sm"
|
||||
style={{display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto'}}>
|
||||
<div>
|
||||
<Dropdown wide>
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
<div style={{minWidth: '200px', display: 'inline-block'}}>
|
||||
{selectedPart ? this._describePart(selectedPart) : 'Unknown'}
|
||||
</div>
|
||||
<i className="fa fa-caret-down fa--skinny space-left"/>
|
||||
</DropdownButton>
|
||||
{parts.map((part, i) => (
|
||||
<DropdownItem key={i} value={i} onClick={this._handleSelectPart}>
|
||||
{i === activePart ? <i className="fa fa-check"/> : <i className="fa fa-empty"/>}
|
||||
{this._describePart(part)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Dropdown right>
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
<i className="fa fa-bars"/>
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={this._handleViewHeaders}>
|
||||
<i className="fa fa-list"/> View Headers
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this._handleSaveAsFile}>
|
||||
<i className="fa fa-save"/> Save as File
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{selectedPart ? (
|
||||
<div className="tall wide">
|
||||
<ResponseViewer
|
||||
key={`${responseId}::${activePart}`}
|
||||
getBody={this._getBody}
|
||||
responseId={`${responseId}[${activePart}]`}
|
||||
previewMode={PREVIEW_MODE_FRIENDLY}
|
||||
filter={filter}
|
||||
filterHistory={filterHistory}
|
||||
editorFontSize={editorFontSize}
|
||||
editorIndentSize={editorIndentSize}
|
||||
editorKeyMap={editorKeyMap}
|
||||
editorLineWrapping={editorLineWrapping}
|
||||
url={url}
|
||||
bytes={selectedPart.bytes || 0}
|
||||
contentType={getContentTypeFromHeaders(selectedPart.headers, 'text/plain')}
|
||||
updateFilter={null}
|
||||
error={null}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResponseMultipart;
|
@ -6,6 +6,7 @@ import {shell} from 'electron';
|
||||
import PDFViewer from '../pdf-viewer';
|
||||
import CodeEditor from '../codemirror/code-editor';
|
||||
import ResponseWebView from './response-webview';
|
||||
import MultipartViewer from './response-multipart';
|
||||
import ResponseRaw from './response-raw';
|
||||
import ResponseError from './response-error';
|
||||
import {LARGE_RESPONSE_MB, PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW} from '../../../common/constants';
|
||||
@ -27,8 +28,8 @@ type Props = {
|
||||
contentType: string,
|
||||
|
||||
// Optional
|
||||
updateFilter?: Function,
|
||||
error?: string
|
||||
updateFilter: Function | null,
|
||||
error: string | null
|
||||
};
|
||||
|
||||
type State = {
|
||||
@ -244,6 +245,21 @@ class ResponseViewer extends React.Component<Props, State> {
|
||||
<PDFViewer body={bodyBuffer} uniqueKey={responseId}/>
|
||||
</div>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('multipart/') === 0) {
|
||||
return (
|
||||
<MultipartViewer
|
||||
responseId={responseId}
|
||||
bodyBuffer={bodyBuffer}
|
||||
contentType={contentType}
|
||||
filter={filter}
|
||||
filterHistory={filterHistory}
|
||||
editorFontSize={editorFontSize}
|
||||
editorIndentSize={editorIndentSize}
|
||||
editorKeyMap={editorKeyMap}
|
||||
editorLineWrapping={editorLineWrapping}
|
||||
url={url}
|
||||
/>
|
||||
);
|
||||
} else if (previewMode === PREVIEW_MODE_FRIENDLY && ct.indexOf('audio/') === 0) {
|
||||
const justContentType = contentType.split(';')[0];
|
||||
const base64Body = bodyBuffer.toString('base64');
|
||||
|
@ -11,6 +11,7 @@ import autobind from 'autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import {registerModal, showModal} from './modals/index';
|
||||
import AlertModal from './modals/alert-modal';
|
||||
import WrapperModal from './modals/wrapper-modal';
|
||||
import ErrorModal from './modals/error-modal';
|
||||
import ChangelogModal from './modals/changelog-modal';
|
||||
import CookiesModal from './modals/cookies-modal';
|
||||
@ -405,6 +406,7 @@ class Wrapper extends React.PureComponent<Props, State> {
|
||||
<ErrorModal ref={registerModal}/>
|
||||
<PromptModal ref={registerModal}/>
|
||||
|
||||
<WrapperModal ref={registerModal}/>
|
||||
<ChangelogModal ref={registerModal}/>
|
||||
<LoginModal ref={registerModal}/>
|
||||
<AskModal ref={registerModal}/>
|
||||
|
@ -78,6 +78,7 @@
|
||||
|
||||
& > * {
|
||||
display: inline-block;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
3
flow-typed/moment.js
vendored
3
flow-typed/moment.js
vendored
@ -1,7 +1,8 @@
|
||||
declare type moment = {
|
||||
fromNow: () => string;
|
||||
format: (fmt: string) => string;
|
||||
};
|
||||
|
||||
declare module 'moment' {
|
||||
declare module.exports: (date: any) => moment
|
||||
declare module.exports: (date?: any) => moment
|
||||
}
|
||||
|
3
flow-typed/multiparty.js
vendored
Normal file
3
flow-typed/multiparty.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare module 'multiparty' {
|
||||
declare module.exports: *
|
||||
}
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -4027,7 +4027,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
|
||||
"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pend": "1.2.0"
|
||||
}
|
||||
@ -7334,6 +7333,14 @@
|
||||
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
|
||||
"dev": true
|
||||
},
|
||||
"multiparty": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.1.3.tgz",
|
||||
"integrity": "sha1-PEPH/LGJbhdGBDap3Qtu8WaOT5Q=",
|
||||
"requires": {
|
||||
"fd-slicer": "1.0.1"
|
||||
}
|
||||
},
|
||||
"mute-stream": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
|
||||
@ -8146,8 +8153,7 @@
|
||||
"pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
|
||||
"dev": true
|
||||
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
|
@ -148,6 +148,7 @@
|
||||
"mime-types": "^2.1.14",
|
||||
"mkdirp": "^0.5.1",
|
||||
"moment": "^2.18.1",
|
||||
"multiparty": "^4.1.3",
|
||||
"nedb": "^1.8.0",
|
||||
"node-forge": "^0.7.0",
|
||||
"nunjucks": "^3.0.0",
|
||||
|
Loading…
Reference in New Issue
Block a user