mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
Merge branch 'main' into bugfix/bug-884
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
"codemirror-graphql": "^1.2.5",
|
||||
"cookie": "^0.6.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.2.9",
|
||||
@@ -38,6 +39,8 @@
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"jsonlint": "^1.6.3",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
@@ -45,6 +48,7 @@
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.3",
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"platform": "^1.3.6",
|
||||
"posthog-node": "^2.1.0",
|
||||
"qs": "^6.11.0",
|
||||
@@ -56,10 +60,12 @@
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-inspector": "^6.0.2",
|
||||
"react-pdf": "^7.5.1",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"system": "^2.0.1",
|
||||
"tailwindcss": "^2.2.19",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
|
||||
@@ -10,12 +10,85 @@ import isEqual from 'lodash/isEqual';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import jsonlint from 'jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
window.JSHINT = JSHINT;
|
||||
//This should be done dynamically if possible
|
||||
const hintWords = [
|
||||
'res',
|
||||
'res.status',
|
||||
'res.statusText',
|
||||
'res.headers',
|
||||
'res.body',
|
||||
'res.getStatus()',
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'req',
|
||||
'req.url',
|
||||
'req.method',
|
||||
'req.headers',
|
||||
'req.body',
|
||||
'req.timeout',
|
||||
'req.getUrl()',
|
||||
'req.setUrl(url)',
|
||||
'req.getMethod()',
|
||||
'req.setMethod(method)',
|
||||
'req.getHeader(name)',
|
||||
'req.getHeaders()',
|
||||
'req.setHeader(name, value)',
|
||||
'req.setHeaders(data)',
|
||||
'req.getBody()',
|
||||
'req.setBody(data)',
|
||||
'req.setMaxRedirects(maxRedirects)',
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName(key)',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
let startBru = cursor.ch;
|
||||
let endBru = startBru;
|
||||
while (endBru < currentLine.length && /[\w.]/.test(currentLine.charAt(endBru))) ++endBru;
|
||||
while (startBru && /[\w.]/.test(currentLine.charAt(startBru - 1))) --startBru;
|
||||
let curWordBru = startBru != endBru && currentLine.slice(startBru, endBru);
|
||||
|
||||
let start = cursor.ch;
|
||||
let end = start;
|
||||
while (end < currentLine.length && /[\w]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[\w]/.test(currentLine.charAt(start - 1))) --start;
|
||||
const jsHinter = CodeMirror.hint.javascript;
|
||||
let result = jsHinter(editor) || { list: [] };
|
||||
result.to = CodeMirror.Pos(cursor.line, end);
|
||||
result.from = CodeMirror.Pos(cursor.line, start);
|
||||
if (curWordBru) {
|
||||
hintWords.forEach((h) => {
|
||||
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
|
||||
result.list.push(curWordBru.includes('.') ? h.split('.')[1] : h);
|
||||
}
|
||||
});
|
||||
result.list?.sort();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||
cm.showHint({ hint, ...options });
|
||||
};
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
@@ -41,7 +114,8 @@ export default class CodeEditor extends React.Component {
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
lint: { esversion: 11 },
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
@@ -68,9 +142,16 @@ export default class CodeEditor extends React.Component {
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
Tab: function (cm) {
|
||||
cm.replaceSelection(' ', 'end');
|
||||
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
||||
? cm.execCommand('indentMore')
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
@@ -103,9 +184,30 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
if (this.props.mode == 'javascript') {
|
||||
editor.on('keyup', function (cm, event) {
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
let start = cursor.ch;
|
||||
let end = start;
|
||||
while (end < currentLine.length && /[^{}();\s\[\]\,]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[^{}();\s\[\]\,]/.test(currentLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && currentLine.slice(start, end);
|
||||
//Qualify if autocomplete will be shown
|
||||
if (
|
||||
/^(?!Shift|Tab|Enter|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|\s)\w*/.test(event.key) &&
|
||||
curWord.length > 0 &&
|
||||
!/\/\/|\/\*|.*{{|`[^$]*{|`[^{]*$/.test(currentLine.slice(0, end)) &&
|
||||
/(?<!\d)[a-zA-Z\._]$/.test(curWord)
|
||||
) {
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.brunoJS, { completeSingle: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -172,6 +274,7 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onEdit) {
|
||||
this.props.onEdit(this.cachedValue);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
td {
|
||||
&:first-child {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
function countRequests(items) {
|
||||
let count = 0;
|
||||
@@ -20,9 +20,9 @@ function countRequests(items) {
|
||||
return count;
|
||||
}
|
||||
|
||||
const CollectionProperties = ({ collection, onClose }) => {
|
||||
const Info = ({ collection }) => {
|
||||
return (
|
||||
<Modal size="sm" title="Collection Properties" hideFooter={true} handleCancel={onClose}>
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
@@ -43,8 +43,8 @@ const CollectionProperties = ({ collection, onClose }) => {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionProperties;
|
||||
export default Info;
|
||||
@@ -0,0 +1,27 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.15rem 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
brunoConfig: { presets: presets = {} }
|
||||
} = collection;
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestType: presets.requestType || 'http',
|
||||
requestUrl: presets.requestUrl || ''
|
||||
},
|
||||
onSubmit: (newPresets) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.presets = newPresets;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('Collection presets updated');
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Collection Presets</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Request Type
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="http"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="http"
|
||||
checked={formik.values.requestType === 'http'}
|
||||
/>
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="graphql"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="graphql"
|
||||
checked={formik.values.requestType === 'graphql'}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="requestUrl">
|
||||
Base URL
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PresetsSettings;
|
||||
@@ -11,22 +11,20 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
is: 'true',
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
.max(65535)
|
||||
.typeError('Specify port between 1 and 65535')
|
||||
.nullable()
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
is: 'true',
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
|
||||
@@ -13,6 +13,8 @@ import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Docs from './Docs';
|
||||
import Presets from './Presets';
|
||||
import Info from './Info';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
@@ -84,6 +86,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'tests': {
|
||||
return <Test collection={collection} />;
|
||||
}
|
||||
case 'presets': {
|
||||
return <Presets collection={collection} />;
|
||||
}
|
||||
case 'proxy': {
|
||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||
}
|
||||
@@ -99,6 +104,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'docs': {
|
||||
return <Docs collection={collection} />;
|
||||
}
|
||||
case 'info': {
|
||||
return <Info collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,6 +131,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => setTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
|
||||
Presets
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
</div>
|
||||
@@ -132,6 +143,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<div className={getTabClassname('info')} role="tab" onClick={() => setTab('info')}>
|
||||
Info
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex ${['auth', 'script', 'docs', 'clientCert'].includes(tab) ? '' : 'mt-4'}`}>
|
||||
{getTabPanel(tab)}
|
||||
|
||||
@@ -32,7 +32,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
secret: Yup.boolean(),
|
||||
type: Yup.string(),
|
||||
uid: Yup.string(),
|
||||
value: Yup.string().trim()
|
||||
value: Yup.string().trim().nullable()
|
||||
})
|
||||
),
|
||||
onSubmit: (values) => {
|
||||
@@ -114,7 +114,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={formik.values[index].name}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
|
||||
@@ -22,13 +22,11 @@ const ProxySettings = ({ close }) => {
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
.max(65535)
|
||||
.typeError('Specify port between 1 and 65535')
|
||||
.nullable()
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
|
||||
@@ -34,7 +34,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
requestUrlChanged({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
url: value
|
||||
url: value && typeof value === 'string' ? value.trim() : value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ const CollectionToolBar = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex flex-1 items-center">
|
||||
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,11 @@ import CodeEditor from 'components/CodeEditor/index';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { useState } from 'react';
|
||||
import 'pdfjs-dist/build/pdf.worker';
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
|
||||
const QueryResultPreview = ({
|
||||
previewTab,
|
||||
@@ -19,6 +24,10 @@ const QueryResultPreview = ({
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [numPages, setNumPages] = useState(null);
|
||||
function onDocumentLoadSuccess({ numPages }) {
|
||||
setNumPages(numPages);
|
||||
}
|
||||
// Fail safe, so we don't render anything with an invalid tab
|
||||
if (!allowedPreviewModes.includes(previewTab)) {
|
||||
return null;
|
||||
@@ -45,6 +54,17 @@ const QueryResultPreview = ({
|
||||
case 'preview-image': {
|
||||
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
|
||||
}
|
||||
case 'preview-pdf': {
|
||||
return (
|
||||
<div className="preview-pdf" style={{ height: '100%', overflow: 'auto', maxHeight: 'calc(100vh - 220px)' }}>
|
||||
<Document file={`data:application/pdf;base64,${dataBuffer}`} onLoadSuccess={onDocumentLoadSuccess}>
|
||||
{Array.from(new Array(numPages), (el, index) => (
|
||||
<Page key={`page_${index + 1}`} pageNumber={index + 1} renderAnnotationLayer={false} />
|
||||
))}
|
||||
</Document>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default:
|
||||
case 'raw': {
|
||||
return (
|
||||
|
||||
@@ -18,6 +18,19 @@ const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-pdf__Page {
|
||||
margin-top: 10px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.react-pdf__Page__textContent {
|
||||
border: 1px solid darkgrey;
|
||||
box-shadow: 5px 5px 5px 1px #ccc;
|
||||
border-radius: 0px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.react-pdf__Page__canvas {
|
||||
margin: 0 auto;
|
||||
}
|
||||
div[role='tablist'] {
|
||||
.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
|
||||
@@ -49,6 +49,8 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
allowedPreviewModes.unshift('preview-web');
|
||||
} else if (mode.includes('image')) {
|
||||
allowedPreviewModes.unshift('preview-image');
|
||||
} else if (contentType.includes('pdf')) {
|
||||
allowedPreviewModes.unshift('preview-pdf');
|
||||
}
|
||||
|
||||
return allowedPreviewModes;
|
||||
|
||||
@@ -9,10 +9,11 @@ const CodeView = ({ language, item }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { target, client, language: lang } = language;
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
let snippet = '';
|
||||
|
||||
try {
|
||||
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
|
||||
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snippet = 'Error generating code snippet';
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { uuid } from 'utils/common';
|
||||
import filter from 'lodash/filter';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
@@ -15,7 +14,6 @@ import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import CollectionItem from './CollectionItem';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import ExportCollection from './ExportCollection';
|
||||
import CollectionProperties from './CollectionProperties';
|
||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections';
|
||||
import exportCollection from 'utils/collections/export';
|
||||
@@ -29,7 +27,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [collectionPropertiesModal, setCollectionPropertiesModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -80,9 +77,14 @@ const Collection = ({ collection, searchText }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportClick = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
const viewCollectionSettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'collection-settings'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
@@ -131,9 +133,6 @@ const Collection = ({ collection, searchText }) => {
|
||||
{showExportCollectionModal && (
|
||||
<ExportCollection collection={collection} onClose={() => setShowExportCollectionModal(false)} />
|
||||
)}
|
||||
{collectionPropertiesModal && (
|
||||
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
|
||||
)}
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
@@ -201,19 +200,19 @@ const Collection = ({ collection, searchText }) => {
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setCollectionPropertiesModal(true);
|
||||
setShowRemoveCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Properties
|
||||
Remove
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRemoveCollectionModal(true);
|
||||
viewCollectionSettings();
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
Settings
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,10 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea.curl-command {
|
||||
min-height: 150px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -16,12 +16,36 @@ import { getRequestFromCurlCommand } from 'utils/curl';
|
||||
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const {
|
||||
brunoConfig: { presets: collectionPresets = {} }
|
||||
} = collection;
|
||||
|
||||
const getRequestType = (collectionPresets) => {
|
||||
if (!collectionPresets || !collectionPresets.requestType) {
|
||||
return 'http-request';
|
||||
}
|
||||
|
||||
// Note: Why different labels for the same thing?
|
||||
// http-request and graphql-request are used inside the app's json representation of a request
|
||||
// http and graphql are used in Bru DSL as well as collection exports
|
||||
// We need to eventually standardize the app's DSL to use the same labels as bru DSL
|
||||
if (collectionPresets.requestType === 'http') {
|
||||
return 'http-request';
|
||||
}
|
||||
|
||||
if (collectionPresets.requestType === 'graphql') {
|
||||
return 'graphql-request';
|
||||
}
|
||||
|
||||
return 'http-request';
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestName: '',
|
||||
requestType: 'http-request',
|
||||
requestUrl: '',
|
||||
requestType: getRequestType(collectionPresets),
|
||||
requestUrl: collectionPresets.requestUrl || '',
|
||||
requestMethod: 'GET',
|
||||
curlCommand: ''
|
||||
},
|
||||
@@ -115,6 +139,25 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(event) => {
|
||||
const clipboardData = event.clipboardData || window.clipboardData;
|
||||
const pastedData = clipboardData.getData('Text');
|
||||
|
||||
// Check if pasted data looks like a cURL command
|
||||
const curlCommandRegex = /^\s*curl\s/i;
|
||||
if (curlCommandRegex.test(pastedData)) {
|
||||
// Switch to the 'from-curl' request type
|
||||
formik.setFieldValue('requestType', 'from-curl');
|
||||
formik.setFieldValue('curlCommand', pastedData);
|
||||
|
||||
// Prevent the default paste behavior to avoid pasting into the textarea
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
@@ -174,7 +217,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
id="request-name"
|
||||
type="text"
|
||||
name="requestName"
|
||||
ref={inputRef}
|
||||
@@ -216,6 +259,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
onPaste={handlePaste}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,8 +276,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
<textarea
|
||||
name="curlCommand"
|
||||
placeholder="Enter cURL request here.."
|
||||
className="block textbox w-full mt-4"
|
||||
style={{ resize: 'none' }}
|
||||
className="block textbox w-full mt-4 curl-command"
|
||||
value={formik.values.curlCommand}
|
||||
onChange={formik.handleChange}
|
||||
></textarea>
|
||||
|
||||
@@ -105,7 +105,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.1.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.1.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,11 @@ if (!SERVER_RENDERED) {
|
||||
require('codemirror/addon/fold/brace-fold');
|
||||
require('codemirror/addon/fold/foldgutter');
|
||||
require('codemirror/addon/fold/xml-fold');
|
||||
require('codemirror/addon/hint/javascript-hint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/lint/javascript-lint');
|
||||
require('codemirror/addon/lint/json-lint');
|
||||
require('codemirror/addon/mode/overlay');
|
||||
require('codemirror/addon/scroll/simplescrollbars');
|
||||
require('codemirror/addon/search/jump-to-line');
|
||||
|
||||
@@ -9,25 +9,17 @@ const createContentType = (mode) => {
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
default:
|
||||
return 'application/json';
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const createHeaders = (headers, mode) => {
|
||||
const contentType = createContentType(mode);
|
||||
const headersArray = headers
|
||||
const createHeaders = (headers) => {
|
||||
return headers
|
||||
.filter((header) => header.enabled)
|
||||
.map((header) => {
|
||||
return {
|
||||
name: header.name,
|
||||
value: header.value
|
||||
};
|
||||
});
|
||||
const headerNames = headersArray.map((header) => header.name);
|
||||
if (!headerNames.includes('Content-Type')) {
|
||||
return [...headersArray, { name: 'Content-Type', value: contentType }];
|
||||
}
|
||||
return headersArray;
|
||||
.map((header) => ({
|
||||
name: header.name,
|
||||
value: header.value
|
||||
}));
|
||||
};
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
@@ -56,13 +48,13 @@ const createPostData = (body) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const buildHarRequest = (request) => {
|
||||
export const buildHarRequest = ({ request, headers }) => {
|
||||
return {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
httpVersion: 'HTTP/1.1',
|
||||
cookies: [],
|
||||
headers: createHeaders(request.headers, request.body.mode),
|
||||
headers: createHeaders(headers),
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body),
|
||||
headersSize: 0,
|
||||
|
||||
@@ -46,8 +46,8 @@ describe('curlToJson', () => {
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: '%27https://www.usebruno.com%27',
|
||||
raw_url: "'https://www.usebruno.com'",
|
||||
url: 'https://www.usebruno.com',
|
||||
raw_url: 'https://www.usebruno.com',
|
||||
method: 'post',
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
|
||||
@@ -187,18 +187,24 @@ const transformOpenapiRequestItem = (request) => {
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const resolveRefs = (spec, components = spec.components) => {
|
||||
const resolveRefs = (spec, components = spec.components, visitedItems = new Set()) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return spec;
|
||||
}
|
||||
|
||||
if (Array.isArray(spec)) {
|
||||
return spec.map((item) => resolveRefs(item, components));
|
||||
return spec.map((item) => resolveRefs(item, components, visitedItems));
|
||||
}
|
||||
|
||||
if ('$ref' in spec) {
|
||||
const refPath = spec.$ref;
|
||||
|
||||
if (visitedItems.has(refPath)) {
|
||||
return spec;
|
||||
} else {
|
||||
visitedItems.add(refPath);
|
||||
}
|
||||
|
||||
if (refPath.startsWith('#/components/')) {
|
||||
// Local reference within components
|
||||
const refKeys = refPath.replace('#/components/', '').split('/');
|
||||
@@ -213,7 +219,7 @@ const resolveRefs = (spec, components = spec.components) => {
|
||||
}
|
||||
}
|
||||
|
||||
return resolveRefs(ref, components);
|
||||
return resolveRefs(ref, components, visitedItems);
|
||||
} else {
|
||||
// Handle external references (not implemented here)
|
||||
// You would need to fetch the external reference and resolve it.
|
||||
@@ -223,7 +229,7 @@ const resolveRefs = (spec, components = spec.components) => {
|
||||
|
||||
// Recursively resolve references in nested objects
|
||||
for (const prop in spec) {
|
||||
spec[prop] = resolveRefs(spec[prop], components);
|
||||
spec[prop] = resolveRefs(spec[prop], components, visitedItems);
|
||||
}
|
||||
|
||||
return spec;
|
||||
@@ -267,12 +273,7 @@ const getDefaultUrl = (serverObject) => {
|
||||
};
|
||||
|
||||
const getSecurity = (apiSpec) => {
|
||||
let supportedSchemes = apiSpec.security || [];
|
||||
if (supportedSchemes.length === 0) {
|
||||
return {
|
||||
supported: []
|
||||
};
|
||||
}
|
||||
let defaultSchemes = apiSpec.security || [];
|
||||
|
||||
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
|
||||
if (Object.keys(securitySchemes) === 0) {
|
||||
@@ -282,7 +283,7 @@ const getSecurity = (apiSpec) => {
|
||||
}
|
||||
|
||||
return {
|
||||
supported: supportedSchemes.map((scheme) => {
|
||||
supported: defaultSchemes.map((scheme) => {
|
||||
var schemeName = Object.keys(scheme)[0];
|
||||
return securitySchemes[schemeName];
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Upgraded axios to 1.5.1
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Announcing Stable Release
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.9.1",
|
||||
"@usebruno/js": "0.9.2",
|
||||
"@usebruno/lang": "0.9.0",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
|
||||
@@ -3,7 +3,7 @@ const qs = require('qs');
|
||||
const chalk = require('chalk');
|
||||
const decomment = require('decomment');
|
||||
const fs = require('fs');
|
||||
const { forOwn, each, extend, get, compact } = require('lodash');
|
||||
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
@@ -136,14 +136,15 @@ const runSingleRequest = async function (
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.1.0",
|
||||
"version": "v1.1.1",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.425.0",
|
||||
"@usebruno/js": "0.9.1",
|
||||
"@usebruno/js": "0.9.2",
|
||||
"@usebruno/lang": "0.9.0",
|
||||
"@usebruno/schema": "0.6.0",
|
||||
"about-window": "^1.15.2",
|
||||
|
||||
@@ -6,11 +6,10 @@ const axios = require('axios');
|
||||
const path = require('path');
|
||||
const decomment = require('decomment');
|
||||
const Mustache = require('mustache');
|
||||
const FormData = require('form-data');
|
||||
const contentDispositionParser = require('content-disposition');
|
||||
const mime = require('mime-types');
|
||||
const { ipcMain } = require('electron');
|
||||
const { forOwn, extend, each, get, compact } = require('lodash');
|
||||
const { isUndefined, isNull, each, get, compact } = require('lodash');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
|
||||
@@ -72,22 +71,6 @@ const getEnvVars = (environment = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getSize = (data) => {
|
||||
if (!data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return Buffer.byteLength(data, 'utf8');
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
return Buffer.byteLength(safeStringifyJSON(data), 'utf8');
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const configureRequest = async (
|
||||
collectionUid,
|
||||
request,
|
||||
@@ -143,7 +126,7 @@ const configureRequest = async (
|
||||
|
||||
// proxy configuration
|
||||
let proxyConfig = get(brunoConfig, 'proxy', {});
|
||||
let proxyEnabled = get(proxyConfig, 'enabled', false);
|
||||
let proxyEnabled = get(proxyConfig, 'enabled', 'global');
|
||||
if (proxyEnabled === 'global') {
|
||||
proxyConfig = preferencesUtil.getGlobalProxyConfig();
|
||||
proxyEnabled = get(proxyConfig, 'enabled', false);
|
||||
@@ -156,14 +139,15 @@ const configureRequest = async (
|
||||
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
@@ -203,10 +187,10 @@ const configureRequest = async (
|
||||
const parseDataFromResponse = (response) => {
|
||||
const dataBuffer = Buffer.from(response.data);
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charset = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
|
||||
const charset = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
|
||||
// Overwrite the original data for backwards compatability
|
||||
let data = dataBuffer.toString(charset || 'utf-8');
|
||||
// Try to parse response to JSON, this can quitly fail
|
||||
// Try to parse response to JSON, this can quietly fail
|
||||
try {
|
||||
data = JSON.parse(response.data);
|
||||
} catch {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/js",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
|
||||
@@ -45,7 +45,7 @@ class Bru {
|
||||
|
||||
setEnvVar(key, value) {
|
||||
if (!key) {
|
||||
throw new Error('Key is required');
|
||||
throw new Error('Creating a env variable without specifying a name is not allowed.');
|
||||
}
|
||||
|
||||
// gracefully ignore if key is not present in environment
|
||||
@@ -58,7 +58,7 @@ class Bru {
|
||||
|
||||
setVar(key, value) {
|
||||
if (!key) {
|
||||
throw new Error('Key is required');
|
||||
throw new Error('Creating a variable without specifying a name is not allowed.');
|
||||
}
|
||||
|
||||
if (envVariableNameRegex.test(key) === false) {
|
||||
|
||||
Reference in New Issue
Block a user