feat(network): copy as curl #220

This commit is contained in:
redhoodsu
2022-12-11 21:49:17 +08:00
parent 40bd9e6ddf
commit b8301620c6
7 changed files with 208 additions and 82 deletions

View File

@@ -13,28 +13,8 @@
padding-bottom: 0;
}
.control {
@include absolute(100%, 40px);
cursor: default;
font-size: 0;
@include control();
padding: 10px 10px 10px 35px;
background: var(--darker-background);
color: var(--primary);
line-height: 20px;
border-bottom: 1px solid var(--border);
.icon-clear,
.icon-filter,
.icon-copy {
display: inline-block;
padding: 10px;
font-size: $font-size-l;
position: absolute;
top: 0;
cursor: pointer;
transition: color $anim-duration;
&:active {
color: var(--accent);
}
}
.icon-clear {
padding-right: 0px;
left: 0;

View File

@@ -1,14 +1,16 @@
import Emitter from 'licia/Emitter'
import trim from 'licia/trim'
import isEmpty from 'licia/isEmpty'
import map from 'licia/map'
import escape from 'licia/escape'
import copy from 'licia/copy'
import extend from 'licia/extend'
import { classPrefix as c } from '../lib/util'
import { curlStr } from './util'
export default class Detail extends Emitter {
constructor($container) {
super()
export default class Detail {
constructor($container, devtools) {
this._$container = $container
this._devtools = devtools
this._detailData = {}
this._bindEvent()
@@ -54,9 +56,10 @@ export default class Detail extends Emitter {
resTxt = `<pre class="${c('response')}">${escape(data.resTxt)}</pre>`
}
const html = `<div class="${c('breadcrumb')}">
const html = `<div class="${c('control')}">
<span class="${c('icon-arrow-left back')}"></span>
${escape(data.url)}
<span class="${c('url')}">${escape(data.url)}</span>
<span class="${c('icon-copy copy-curl')}"></span>
</div>
<div class="${c('http')}">
${postData}
@@ -85,9 +88,43 @@ export default class Detail extends Emitter {
hide() {
this._$container.hide()
}
_copyCurl = () => {
const detailData = this._detailData
copy(
curlStr({
requestMethod: detailData.method,
url() {
return detailData.url
},
requestFormData() {
return detailData.data
},
requestHeaders() {
const reqHeaders = detailData.reqHeaders || {}
extend(reqHeaders, {
'User-Agent': navigator.userAgent,
Referer: location.href,
})
return map(reqHeaders, (value, name) => {
return {
name,
value,
}
})
},
})
)
this._devtools.notify('Copied')
}
_bindEvent() {
const devtools = this._devtools
this._$container
.on('click', c('.back'), () => this.hide())
.on('click', c('.copy-curl'), this._copyCurl)
.on('click', c('.http .response'), () => {
const data = this._detailData
const resTxt = data.resTxt
@@ -109,7 +146,14 @@ export default class Detail extends Emitter {
})
const showSources = (type, data) => {
this.emit('showSources', type, data)
const sources = devtools.get('sources')
if (!sources) {
return
}
sources.set(type, data)
devtools.showTool('sources')
}
}
}

View File

@@ -2,7 +2,6 @@ import Tool from '../DevTools/Tool'
import $ from 'licia/$'
import ms from 'licia/ms'
import each from 'licia/each'
import last from 'licia/last'
import Detail from './Detail'
import throttle from 'licia/throttle'
import { getFileName, classPrefix as c } from '../lib/util'
@@ -10,6 +9,7 @@ import evalCss from '../lib/evalCss'
import chobitsu from '../lib/chobitsu'
import LunaDataGrid from 'luna-data-grid'
import ResizeSensor from 'licia/ResizeSensor'
import { getType } from './util'
export default class Network extends Tool {
constructor() {
@@ -25,7 +25,7 @@ export default class Network extends Tool {
this._container = container
this._initTpl()
this._detail = new Detail(this._$detail)
this._detail = new Detail(this._$detail, container)
this._requestDataGrid = new LunaDataGrid(this._$requests.get(0), {
columns: [
{
@@ -183,7 +183,6 @@ export default class Network extends Tool {
}
_bindEvent() {
const $el = this._$el
const container = this._container
const self = this
@@ -198,15 +197,6 @@ export default class Network extends Tool {
self._detail.show(request)
})
this._detail.on('showSources', function (type, data) {
const sources = container.get('sources')
if (!sources) return
sources.set(type, data)
container.showTool('sources')
})
this._resizeSensor.addListener(
throttle(() => this._updateDataGridHeight(), 15)
)
@@ -247,14 +237,3 @@ export default class Network extends Tool {
this._$requests = $el.find(c('.requests'))
}
}
function getType(contentType) {
if (!contentType) return 'unknown'
const type = contentType.split(';')[0].split('/')
return {
type: type[0],
subType: last(type),
}
}

View File

@@ -29,29 +29,23 @@
display: none;
padding-top: 40px;
background: var(--background);
.breadcrumb {
@include breadcrumb();
@include absolute(100%, 40px);
font-size: $font-size-s;
.control {
@include control();
padding: 10px 35px;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
.icon-arrow-left {
.url {
font-size: $font-size-s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
display: inline-block;
padding: 10px;
font-size: $font-size-l;
position: absolute;
top: 0;
cursor: pointer;
transition: color $anim-duration;
&:active {
color: var(--accent);
}
}
.icon-arrow-left {
left: 0;
}
.icon-copy {
right: 0;
}
}
.http {
@include overflow-auto(y);
@@ -90,7 +84,7 @@
@include overflow-auto(x);
padding: $padding;
font-size: $font-size-s;
margin-bottom: 10px;
margin: 10px 0;
white-space: pre-wrap;
border-top: 1px solid var(--border);
color: var(--foreground);

106
src/Network/util.js Normal file
View File

@@ -0,0 +1,106 @@
import last from 'licia/last'
import detectOs from 'licia/detectOs'
export function getType(contentType) {
if (!contentType) return 'unknown'
const type = contentType.split(';')[0].split('/')
return {
type: type[0],
subType: last(type),
}
}
export function curlStr(request) {
let platform = detectOs()
if (platform === 'windows') {
platform = 'win'
}
/* eslint-disable */
let command = []
const ignoredHeaders = new Set([
'accept-encoding',
'host',
'method',
'path',
'scheme',
'version',
])
function escapeStringWin(str) {
const encapsChars = /[\r\n]/.test(str) ? '^"' : '"'
return (
encapsChars +
str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/[^a-zA-Z0-9\s_\-:=+~'\/.',?;()*`&]/g, '^$&')
.replace(/%(?=[a-zA-Z0-9_])/g, '%^')
.replace(/\r?\n/g, '^\n\n') +
encapsChars
)
}
function escapeStringPosix(str) {
function escapeCharacter(x) {
const code = x.charCodeAt(0)
let hexString = code.toString(16)
while (hexString.length < 4) {
hexString = '0' + hexString
}
return '\\u' + hexString
}
if (/[\0-\x1F\x7F-\x9F!]|\'/.test(str)) {
return (
"$'" +
str
.replace(/\\/g, '\\\\')
.replace(/\'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) +
"'"
)
}
return "'" + str + "'"
}
const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix
command.push(escapeString(request.url()).replace(/[[{}\]]/g, '\\$&'))
let inferredMethod = 'GET'
const data = []
const formData = request.requestFormData()
if (formData) {
data.push('--data-raw ' + escapeString(formData))
ignoredHeaders.add('content-length')
inferredMethod = 'POST'
}
if (request.requestMethod !== inferredMethod) {
command.push('-X ' + escapeString(request.requestMethod))
}
const requestHeaders = request.requestHeaders()
for (let i = 0; i < requestHeaders.length; i++) {
const header = requestHeaders[i]
const name = header.name.replace(/^:/, '')
if (ignoredHeaders.has(name.toLowerCase())) {
continue
}
command.push('-H ' + escapeString(name + ': ' + header.value))
}
command = command.concat(data)
command.push('--compressed')
return (
'curl ' +
command.join(
command.length >= 3 ? (platform === 'win' ? ' ^\n ' : ' \\\n ') : ' '
)
)
}

View File

@@ -148,22 +148,23 @@ export default {
if (!this._isInit) logger.error('Please call "eruda.init()" first')
return this._isInit
},
_initContainer(el, useShadowDom) {
if (!el) {
el = document.createElement('div')
document.documentElement.appendChild(el)
_initContainer(container, useShadowDom) {
if (!container) {
container = document.createElement('div')
document.documentElement.appendChild(container)
}
el.id = 'eruda'
el.style.all = 'initial'
this._container = el
container.id = 'eruda'
container.style.all = 'initial'
this._container = container
let shadowRoot
let el
if (useShadowDom) {
if (el.attachShadow) {
shadowRoot = el.attachShadow({ mode: 'open' })
} else if (el.createShadowRoot) {
shadowRoot = el.createShadowRoot()
if (container.attachShadow) {
shadowRoot = container.attachShadow({ mode: 'open' })
} else if (container.createShadowRoot) {
shadowRoot = container.createShadowRoot()
}
if (shadowRoot) {
// font-face doesn't work inside shadow dom.
@@ -178,9 +179,8 @@ export default {
}
if (!this._shadowRoot) {
const oldEl = el
el = document.createElement('div')
oldEl.appendChild(el)
container.appendChild(el)
}
extend(el, {

View File

@@ -29,6 +29,29 @@
border-bottom: 1px solid var(--border);
}
@mixin control {
@include absolute(100%, 40px);
cursor: default;
font-size: 0;
background: var(--darker-background);
color: var(--primary);
line-height: 20px;
border-bottom: 1px solid var(--border);
[class^='icon-'],
[class*=' icon-'] {
display: inline-block;
padding: 10px;
font-size: $font-size-l;
position: absolute;
top: 0;
cursor: pointer;
transition: color $anim-duration;
&:active {
color: var(--accent);
}
}
}
@mixin clear-float {
&:after {
content: '';