@@ -146,6 +146,7 @@ displayOperationId | Controls the display of operationId in operations list. The | |||
displayRequestDuration | Controls the display of the request duration (in milliseconds) for `Try it out` requests. The default is `false`. | |||
maxDisplayedTags | If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations. | |||
filter | If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. Can be true/false to enable or disable, or an explicit filter string in which case filtering will be enabled using that string as the filter expression. Filtering is case sensitive matching the filter expression anywhere inside the tag. | |||
deepLinking | If set to `true`, enables dynamic deep linking for tags and operations. [Docs](https://github.com/swagger-api/swagger-ui/blob/master/docs/deep-linking.md) | |||
### Plugins | |||
@@ -235,6 +236,10 @@ Access-Control-Allow-Headers: Content-Type, api_key, Authorization | |||
Only headers with these names will be allowed to be sent by Swagger-UI. | |||
## Security contact | |||
Please disclose any security-related issues or vulnerabilities by emailing [security@swagger.io](mailto:security@swagger.io), instead of using the public issue tracker. | |||
## License | |||
Copyright 2017 SmartBear Software | |||
@@ -76,6 +76,7 @@ window.onload = function() { | |||
const ui = SwaggerUIBundle({ | |||
url: "http://petstore.swagger.io/v2/swagger.json", | |||
dom_id: '#swagger-ui', | |||
deepLinking: true, | |||
presets: [ | |||
SwaggerUIBundle.presets.apis, | |||
SwaggerUIStandalonePreset | |||
@@ -61,7 +61,6 @@ module.exports = function(rules, options) { | |||
} | |||
if( specialOptions.minimize ) { | |||
plugins.push( | |||
new webpack.optimize.UglifyJsPlugin({ | |||
sourceMap: true, | |||
@@ -109,7 +109,6 @@ | |||
"karma-sourcemap-loader": "^0.3.7", | |||
"karma-webpack": "2.0.3", | |||
"less": "2.7.2", | |||
"less-loader": "4.0.4", | |||
"license-checker": "^11.0.0", | |||
"mocha": "^3.4.2", | |||
"node-sass": "^4.5.0", | |||
@@ -15,7 +15,7 @@ class Path extends React.Component { | |||
return ( | |||
<pre className="base-url"> | |||
[ Base url: {host}{basePath}] | |||
[ Base URL: {host}{basePath} ] | |||
</pre> | |||
) | |||
} | |||
@@ -129,7 +129,8 @@ export class Select extends React.Component { | |||
value: PropTypes.any, | |||
onChange: PropTypes.func, | |||
multiple: PropTypes.bool, | |||
allowEmptyValue: PropTypes.bool | |||
allowEmptyValue: PropTypes.bool, | |||
className: PropTypes.string | |||
} | |||
static defaultProps = { | |||
@@ -142,7 +143,7 @@ export class Select extends React.Component { | |||
let value | |||
if (props.value !== undefined) { | |||
if (props.value) { | |||
value = props.value | |||
} else { | |||
value = props.multiple ? [""] : "" | |||
@@ -178,7 +179,7 @@ export class Select extends React.Component { | |||
let value = this.state.value.toJS ? this.state.value.toJS() : this.state.value | |||
return ( | |||
<select multiple={ multiple } value={ value } onChange={ this.onChange } > | |||
<select className={this.props.className} multiple={ multiple } value={ value } onChange={ this.onChange } > | |||
{ allowEmptyValue ? <option value="">--</option> : null } | |||
{ | |||
allowedValues.map(function (item, key) { | |||
@@ -24,8 +24,8 @@ export default class Models extends Component { | |||
return <section className={ showModels ? "models is-open" : "models"}> | |||
<h4 onClick={() => layoutActions.show("models", !showModels)}> | |||
<span>Models</span> | |||
<svg width="20" height="20"> | |||
<use xlinkHref="#large-arrow" /> | |||
<svg className="arrow" width="20" height="20"> | |||
<use xlinkHref={showModels ? "#large-arrow-down" : "#large-arrow"} /> | |||
</svg> | |||
</h4> | |||
<Collapse isOpened={showModels}> | |||
@@ -19,8 +19,9 @@ export default class Schemes extends React.Component { | |||
} | |||
componentWillReceiveProps(nextProps) { | |||
if ( this.props.operationScheme && !nextProps.schemes.has(this.props.operationScheme) ) { | |||
//fire 'change' event if our selected scheme is no longer an option | |||
if ( !this.props.operationScheme || !nextProps.schemes.has(this.props.operationScheme) ) { | |||
// if we don't have a selected operationScheme or if our selected scheme is no longer an option, | |||
// then fire 'change' event and select the first scheme in the list of options | |||
this.setScheme(nextProps.schemes.first()) | |||
} | |||
} | |||
@@ -57,7 +57,8 @@ export class JsonSchema_string extends Component { | |||
if ( enumValue ) { | |||
const Select = getComponent("Select") | |||
return (<Select allowedValues={ enumValue } | |||
return (<Select className={ errors.length ? "invalid" : ""} | |||
allowedValues={ enumValue } | |||
value={ value } | |||
allowEmptyValue={ !required } | |||
onChange={ this.onEnumChange }/>) | |||
@@ -121,6 +122,7 @@ export class JsonSchema_array extends PureComponent { | |||
render() { | |||
let { getComponent, required, schema, fn } = this.props | |||
let errors = schema.errors || [] | |||
let itemSchema = fn.inferSchema(schema.items) | |||
const JsonSchemaForm = getComponent("JsonSchemaForm") | |||
@@ -131,19 +133,17 @@ export class JsonSchema_array extends PureComponent { | |||
if ( enumValue ) { | |||
const Select = getComponent("Select") | |||
return (<Select multiple={ true } | |||
return (<Select className={ errors.length ? "invalid" : ""} | |||
multiple={ true } | |||
value={ value } | |||
allowedValues={ enumValue } | |||
allowEmptyValue={ !required } | |||
onChange={ this.onEnumChange }/>) | |||
} | |||
let errors = schema.errors || [] | |||
return ( | |||
<div> | |||
{ !value || value.count() < 1 ? | |||
(errors.length ? <span style={{ color: "red", fortWeight: "bold" }}>{ errors[0] }</span> : null) : | |||
{ !value || value.count() < 1 ? null : | |||
value.map( (item,i) => { | |||
let schema = Object.assign({}, itemSchema) | |||
if ( errors.length ) { | |||
@@ -153,12 +153,12 @@ export class JsonSchema_array extends PureComponent { | |||
return ( | |||
<div key={i} className="json-schema-form-item"> | |||
<JsonSchemaForm fn={fn} getComponent={getComponent} value={item} onChange={(val) => this.onItemChange(val, i)} schema={schema} /> | |||
<Button className="json-schema-form-item-remove" onClick={()=> this.removeItem(i)} > - </Button> | |||
<Button className="btn btn-sm json-schema-form-item-remove" onClick={()=> this.removeItem(i)} > - </Button> | |||
</div> | |||
) | |||
}).toArray() | |||
} | |||
<Button className="json-schema-form-item-add" onClick={this.addItem}> Add item </Button> | |||
<Button className={`btn btn-sm json-schema-form-item-add ${errors.length ? "invalid" : null}`} onClick={this.addItem}> Add item </Button> | |||
</div> | |||
) | |||
} | |||
@@ -170,12 +170,14 @@ export class JsonSchema_boolean extends Component { | |||
onEnumChange = (val) => this.props.onChange(val) | |||
render() { | |||
let { getComponent, required, value } = this.props | |||
let { getComponent, value, schema } = this.props | |||
let errors = schema.errors || [] | |||
const Select = getComponent("Select") | |||
return (<Select value={ String(value) } | |||
return (<Select className={ errors.length ? "invalid" : ""} | |||
value={ String(value) } | |||
allowedValues={ fromJS(["true", "false"]) } | |||
allowEmptyValue={ !required } | |||
allowEmptyValue={true} | |||
onChange={ this.onEnumChange }/>) | |||
} | |||
} |
@@ -1,67 +0,0 @@ | |||
import get from "lodash/get" | |||
export function transformPathToArray(property, jsSpec) { | |||
if(property.slice(0,9) === "instance.") { | |||
var str = property.slice(9) | |||
} else { // eslint-disable-next-line no-redeclare | |||
var str = property | |||
} | |||
var pathArr = [] | |||
str | |||
.split(".") | |||
.map(item => { | |||
// "key[0]" becomes ["key", "0"] | |||
if(item.includes("[")) { | |||
let index = parseInt(item.match(/\[(.*)\]/)[1]) | |||
let keyName = item.slice(0, item.indexOf("[")) | |||
return [keyName, index.toString()] | |||
} else { | |||
return item | |||
} | |||
}) | |||
.reduce(function(a, b) { | |||
// flatten! | |||
return a.concat(b) | |||
}, []) | |||
.concat([""]) // add an empty item into the array, so we don't get stuck with something in our buffer below | |||
.reduce((buffer, curr) => { | |||
let obj = pathArr.length ? get(jsSpec, pathArr) : jsSpec | |||
if(get(obj, makeAccessArray(buffer, curr))) { | |||
if(buffer.length) { | |||
pathArr.push(buffer) | |||
} | |||
if(curr.length) { | |||
pathArr.push(curr) | |||
} | |||
return "" | |||
} else { | |||
// attach key to buffer | |||
return `${buffer}${buffer.length ? "." : ""}${curr}` | |||
} | |||
}, "") | |||
if(typeof get(jsSpec, pathArr) !== "undefined") { | |||
return pathArr | |||
} else { | |||
// if our path is not correct (there is no value at the path), | |||
// return null | |||
return null | |||
} | |||
} | |||
function makeAccessArray(buffer, curr) { | |||
let arr = [] | |||
if(buffer.length) { | |||
arr.push(buffer) | |||
} | |||
if(curr.length) { | |||
arr.push(curr) | |||
} | |||
return arr | |||
} |
@@ -73,7 +73,7 @@ export const authorizePassword = ( auth ) => ( { authActions } ) => { | |||
let { schema, name, username, password, passwordType, clientId, clientSecret } = auth | |||
let form = { | |||
grant_type: "password", | |||
scopes: encodeURIComponent(auth.scopes.join(scopeSeparator)) | |||
scope: encodeURIComponent(auth.scopes.join(scopeSeparator)) | |||
} | |||
let query = {} | |||
let headers = {} | |||
@@ -1,7 +1,6 @@ | |||
import React from "react" | |||
import PropTypes from "prop-types" | |||
import SplitPane from "react-split-pane" | |||
import "./split-pane-mode.less" | |||
const MODE_KEY = ["split-pane-mode"] | |||
const MODE_LEFT = "left" | |||
@@ -1,5 +0,0 @@ | |||
.swagger-ui { | |||
.Resizer.vertical.disabled { | |||
display: none; | |||
} | |||
} |
@@ -1,8 +1,7 @@ | |||
import { shallowEqualKeys } from "core/utils" | |||
import { transformPathToArray } from "core/path-translator" | |||
export default function() { | |||
return { | |||
fn: { shallowEqualKeys, transformPathToArray } | |||
fn: { shallowEqualKeys } | |||
} | |||
} |
@@ -41,7 +41,7 @@ export function fromJSOrdered (js) { | |||
return !isObject(js) ? js : | |||
Array.isArray(js) ? | |||
Im.Seq(js).map(fromJSOrdered).toList() : | |||
Im.Seq(js).map(fromJSOrdered).toOrderedMap() | |||
Im.OrderedMap(js).map(fromJSOrdered) | |||
} | |||
export function bindToState(obj, state) { | |||
@@ -468,6 +468,18 @@ export const validateFile = ( val ) => { | |||
} | |||
} | |||
export const validateBoolean = ( val ) => { | |||
if ( !(val === "true" || val === "false" || val === true || val === false) ) { | |||
return "Value must be a boolean" | |||
} | |||
} | |||
export const validateString = ( val ) => { | |||
if ( val && typeof val !== "string" ) { | |||
return "Value must be a string" | |||
} | |||
} | |||
// validation of parameters before execute | |||
export const validateParam = (param, isXml) => { | |||
let errors = [] | |||
@@ -475,52 +487,66 @@ export const validateParam = (param, isXml) => { | |||
let required = param.get("required") | |||
let type = param.get("type") | |||
let stringCheck = type === "string" && !value | |||
let arrayCheck = type === "array" && Array.isArray(value) && !value.length | |||
let listCheck = type === "array" && Im.List.isList(value) && !value.count() | |||
let fileCheck = type === "file" && !(value instanceof win.File) | |||
if ( required && (stringCheck || arrayCheck || listCheck || fileCheck) ) { | |||
errors.push("Required field is not provided") | |||
return errors | |||
} | |||
if ( value === null || value === undefined ) { | |||
return errors | |||
} | |||
if ( type === "number" ) { | |||
let err = validateNumber(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
} else if ( type === "integer" ) { | |||
let err = validateInteger(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
} else if ( type === "array" ) { | |||
let itemType | |||
if ( !value.count() ) { return errors } | |||
itemType = param.getIn(["items", "type"]) | |||
value.forEach((item, index) => { | |||
let err | |||
// If the parameter is required OR the parameter has a value (meaning optional, but filled in) | |||
// then we should do our validation routine | |||
if ( required || value ) { | |||
// These checks should evaluate to true if the parameter's value is valid | |||
let stringCheck = type === "string" && value && !validateString(value) | |||
let arrayCheck = type === "array" && Array.isArray(value) && value.length | |||
let listCheck = type === "array" && Im.List.isList(value) && value.count() | |||
let fileCheck = type === "file" && value instanceof win.File | |||
let booleanCheck = type === "boolean" && !validateBoolean(value) | |||
let numberCheck = type === "number" && !validateNumber(value) // validateNumber returns undefined if the value is a number | |||
let integerCheck = type === "integer" && !validateInteger(value) // validateInteger returns undefined if the value is an integer | |||
if ( required && !(stringCheck || arrayCheck || listCheck || fileCheck || booleanCheck || numberCheck || integerCheck) ) { | |||
errors.push("Required field is not provided") | |||
return errors | |||
} | |||
if (itemType === "number") { | |||
err = validateNumber(item) | |||
} else if (itemType === "integer") { | |||
err = validateInteger(item) | |||
} | |||
if ( type === "string" ) { | |||
let err = validateString(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
} else if ( type === "boolean" ) { | |||
let err = validateBoolean(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
} else if ( type === "number" ) { | |||
let err = validateNumber(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
} else if ( type === "integer" ) { | |||
let err = validateInteger(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
} else if ( type === "array" ) { | |||
let itemType | |||
if ( !value.count() ) { return errors } | |||
itemType = param.getIn(["items", "type"]) | |||
value.forEach((item, index) => { | |||
let err | |||
if (itemType === "number") { | |||
err = validateNumber(item) | |||
} else if (itemType === "integer") { | |||
err = validateInteger(item) | |||
} else if (itemType === "string") { | |||
err = validateString(item) | |||
} | |||
if ( err ) { | |||
errors.push({ index: index, error: err}) | |||
} | |||
}) | |||
} else if ( type === "file" ) { | |||
let err = validateFile(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
if ( err ) { | |||
errors.push({ index: index, error: err}) | |||
} | |||
}) | |||
} else if ( type === "file" ) { | |||
let err = validateFile(value) | |||
if (!err) return errors | |||
errors.push(err) | |||
} | |||
} | |||
return errors | |||
@@ -20,7 +20,7 @@ SwaggerUI({ | |||
}) | |||
``` | |||
Or if you're updating the core plugins.. you'll add it to [src/js/bootstrap-plugin](https://github.com/SmartBear/swagger-ux/blob/master/src/js/bootstrap-plugin.js) | |||
Or if you're updating the core plugins.. you'll add it to the base preset: [src/core/presets/base.js](https://github.com/swagger-api/swagger-ui/blob/master/src/core/presets/base.js) | |||
Each Plugin is a function that returns an object. That object will get merged with the `system` and later bound to the state. | |||
Here is an example of each `type` | |||
@@ -130,7 +130,7 @@ export default class Topbar extends React.Component { | |||
<div className="wrapper"> | |||
<div className="topbar-wrapper"> | |||
<Link href="#" title="Swagger UX"> | |||
<img height="30" width="30" src={ Logo } alt="Swagger UX"/> | |||
<img height="30" width="30" src={ Logo } alt="Swagger UI"/> | |||
<span>swagger</span> | |||
</Link> | |||
<form className="download-url-wrapper" onSubmit={formOnSubmit}> | |||
@@ -1,52 +0,0 @@ | |||
.swagger-ui { | |||
.topbar { | |||
background-color: #89bf04; | |||
} | |||
.topbar-wrapper { | |||
padding: 0.7em; | |||
} | |||
.topbar-logo__img { | |||
float: left; | |||
} | |||
.topbar-logo__title { | |||
display: inline-block; | |||
color: #fff; | |||
font-size: 1.5em; | |||
font-weight: bold; | |||
margin: 0.15em 0 0 0.5em; | |||
} | |||
.download-url-wrapper { | |||
text-align: right; | |||
float: right; | |||
} | |||
.topbar .download-url__text { | |||
width: 28em; | |||
height: 2em; | |||
margin-right: 0.5em; | |||
} | |||
.download-url__btn { | |||
background-color: #547f00; | |||
border-color: #547f00; | |||
text-decoration: none; | |||
font-weight: bold; | |||
padding: 0.2em 0.3em; | |||
color: white; | |||
border-radius: 0.1em; | |||
&:hover { | |||
&:extend(.download-url__btn); | |||
} | |||
} | |||
.center-700 { | |||
display: block; | |||
margin: 0 auto; | |||
width: 45em; | |||
} | |||
} |
@@ -1,6 +1,4 @@ | |||
import StandaloneLayout from "./layout" | |||
import "../style/main.scss" | |||
import TopbarPlugin from "plugins/topbar" | |||
import ConfigsPlugin from "plugins/configs" | |||
@@ -14,6 +14,11 @@ | |||
@include text_headline(); | |||
&.btn-sm { | |||
font-size: 12px; | |||
padding: 4px 23px; | |||
} | |||
&[disabled] | |||
{ | |||
cursor: not-allowed; | |||
@@ -165,6 +170,9 @@ | |||
button | |||
{ | |||
cursor: pointer; | |||
outline: none; | |||
&.invalid { | |||
@include invalidFormElement(); | |||
} | |||
} |
@@ -21,6 +21,10 @@ select | |||
background: #f7f7f7; | |||
} | |||
&.invalid { | |||
@include invalidFormElement(); | |||
} | |||
} | |||
.opblock-body select | |||
@@ -53,12 +57,8 @@ input[type=file] | |||
border-radius: 4px; | |||
background: #fff; | |||
&.invalid | |||
{ | |||
animation: shake .4s 1; | |||
border-color: $_color-delete; | |||
background: lighten($_color-delete, 35%); | |||
&.invalid { | |||
@include invalidFormElement(); | |||
} | |||
} | |||
@@ -490,6 +490,15 @@ | |||
{ | |||
margin: 0; | |||
} | |||
a | |||
{ | |||
@include text_code(#89bf04); | |||
text-decoration: underline; | |||
&:hover { | |||
color: #81b10c; | |||
} | |||
} | |||
} | |||
} | |||
@@ -166,3 +166,9 @@ $browser-context: 16; | |||
@warn 'Breakpoint mixin supports: tablet, mobile, desktop'; | |||
} | |||
} | |||
@mixin invalidFormElement() { | |||
animation: shake .4s 1; | |||
border-color: $_color-delete; | |||
background: lighten($_color-delete, 35%); | |||
} |
@@ -0,0 +1,3 @@ | |||
.Resizer.vertical.disabled { | |||
display: none; | |||
} |
@@ -97,6 +97,10 @@ table | |||
width: 100%; | |||
max-width: 340px; | |||
} | |||
select { | |||
border-width: 1px; | |||
} | |||
} | |||
.parameter__name | |||
@@ -14,6 +14,6 @@ | |||
@import 'information'; | |||
@import 'authorize'; | |||
@import 'errors'; | |||
@include text_body(); | |||
@import 'split-pane-mode'; | |||
} |
@@ -0,0 +1,41 @@ | |||
/* eslint-env mocha */ | |||
import React from "react" | |||
import expect, { createSpy } from "expect" | |||
import { shallow } from "enzyme" | |||
import { fromJS } from "immutable" | |||
import Schemes from "components/schemes" | |||
describe("<Schemes/>", function(){ | |||
it("calls props.specActions.setScheme() when no operationScheme is selected", function(){ | |||
// Given | |||
let props = { | |||
specActions: { | |||
setScheme: createSpy() | |||
}, | |||
schemes: fromJS([ | |||
"http", | |||
"https" | |||
]), | |||
operationScheme: undefined, | |||
path: "/test", | |||
method: "get" | |||
} | |||
// When | |||
let wrapper = shallow(<Schemes {...props}/>) | |||
// Then operationScheme should default to first scheme in options list | |||
expect(props.specActions.setScheme).toHaveBeenCalledWith("http", "/test" , "get") | |||
// When the operationScheme is no longer in the list of options | |||
props.schemes = fromJS([ | |||
"https" | |||
]) | |||
wrapper.setProps(props) | |||
// Then operationScheme should default to first scheme in options list | |||
expect(props.specActions.setScheme).toHaveBeenCalledWith("https", "/test", "get") | |||
}) | |||
}) |
@@ -1,183 +0,0 @@ | |||
/* eslint-env mocha */ | |||
import expect from "expect" | |||
import { transformPathToArray } from "core/path-translator" | |||
describe("validation plugin - path translator", function(){ | |||
describe("string paths", function(){ | |||
it("should translate a simple string path to an array", function(){ | |||
// Given | |||
let jsSpec = { | |||
one: { | |||
a: "a thing", | |||
b: "another thing", | |||
c: "one more thing" | |||
}, | |||
two: 2 | |||
} | |||
let path = "instance.one.a" | |||
// Then | |||
expect(transformPathToArray(path, jsSpec)).toEqual(["one", "a"]) | |||
}) | |||
it("should translate an ambiguous string path to an array", function(){ | |||
// Since JSONSchema uses periods to mark different properties, | |||
// a key with a period in it is ambiguous, because it can mean at least two things. | |||
// In our case, the path can mean: | |||
// ["google", "com", "a"] or ["google.com", "a"] | |||
// Given | |||
let jsSpec = { | |||
"google.com": { | |||
a: "a thing", | |||
b: "another thing", | |||
c: "one more thing" | |||
}, | |||
"gmail.com": { | |||
d: "more stuff", | |||
e: "even more stuff" | |||
} | |||
} | |||
let path = "instance.google.com.a" | |||
// Then | |||
expect(transformPathToArray(path, jsSpec)).toEqual(["google.com", "a"]) | |||
}) | |||
it("should translate an doubly ambiguous string path to an array", function(){ | |||
// Since JSONSchema uses periods to mark different properties, | |||
// a key with two periods in it (like "www.google.com") is doubly ambiguous, | |||
// because it can mean at least three things. | |||
// Given | |||
let jsSpec = { | |||
"www.google.com": { | |||
a: "a thing", | |||
b: "another thing", | |||
c: "one more thing" | |||
}, | |||
"gmail.com": { | |||
d: "more stuff", | |||
e: "even more stuff" | |||
} | |||
} | |||
let path = "instance.www.google.com.a" | |||
// Then | |||
expect(transformPathToArray(path, jsSpec)).toEqual(["www.google.com", "a"]) | |||
}) | |||
it("should return null for an invalid path", function(){ | |||
// Given | |||
let jsSpec = { | |||
"google.com": { | |||
a: "a thing", | |||
b: "another thing", | |||
c: "one more thing" | |||
}, | |||
"gmail.com": { | |||
d: "more stuff", | |||
e: "even more stuff" | |||
} | |||
} | |||
let path = "instance.google.net.a" | |||
// Then | |||
expect(transformPathToArray(path, jsSpec)).toEqual(null) | |||
}) | |||
it("should return inline array indices in their own value", function(){ | |||
// "a[1]" => ["a", "1"] | |||
// Given | |||
let jsSpec = { | |||
"google.com": { | |||
a: [ | |||
"hello", | |||
"here is the target" | |||
], | |||
b: "another thing", | |||
c: "one more thing" | |||
}, | |||
"gmail.com": { | |||
d: "more stuff", | |||
e: "even more stuff" | |||
} | |||
} | |||
let path = "instance.google.com.a[1]" | |||
// Then | |||
expect(transformPathToArray(path, jsSpec)).toEqual(["google.com", "a", "1"]) | |||
}) | |||
it("should return the correct path when the last part is ambiguous", function(){ | |||
// Given | |||
let jsSpec = { | |||
"google.com": { | |||
a: [ | |||
"hello", | |||
{ | |||
"gmail.com": 1234 | |||
} | |||
], | |||
b: "another thing", | |||
c: "one more thing" | |||
}, | |||
"gmail.com": { | |||
d: "more stuff", | |||
e: "even more stuff" | |||
} | |||
} | |||
let path = "instance.google.com.a[1].gmail.com" | |||
// Then | |||
expect(transformPathToArray(path, jsSpec)).toEqual(["google.com", "a", "1", "gmail.com"]) | |||
}) | |||
it("should return the correct path when the last part is doubly ambiguous", function(){ | |||
// Given | |||
let jsSpec = { | |||
"google.com": { | |||
a: [ | |||
"hello", | |||
{ | |||
"www.gmail.com": 1234 | |||
} | |||
], | |||
b: "another thing", | |||
c: "one more thing" | |||
}, | |||
"gmail.com": { | |||
d: "more stuff", | |||
e: "even more stuff" | |||
} | |||
} | |||
let path = "instance.google.com.a[1].www.gmail.com" | |||
// Then | |||
expect(transformPathToArray(path, jsSpec)).toEqual(["google.com", "a", "1", "www.gmail.com"]) | |||
}) | |||
}) | |||
}) |
@@ -1,10 +1,10 @@ | |||
/* eslint-env mocha */ | |||
import expect from "expect" | |||
import { fromJS } from "immutable" | |||
import { mapToList, validateNumber, validateInteger, validateParam, validateFile } from "core/utils" | |||
import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered } from "core/utils" | |||
import win from "core/window" | |||
describe("utils", function(){ | |||
describe("utils", function() { | |||
describe("mapToList", function(){ | |||
@@ -176,6 +176,7 @@ describe("utils", function(){ | |||
let result = null | |||
it("validates required strings", function() { | |||
// invalid string | |||
param = fromJS({ | |||
required: true, | |||
type: "string", | |||
@@ -183,9 +184,39 @@ describe("utils", function(){ | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// valid string | |||
param = fromJS({ | |||
required: true, | |||
type: "string", | |||
value: "test string" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates optional strings", function() { | |||
// valid (empty) string | |||
param = fromJS({ | |||
required: false, | |||
type: "string", | |||
value: "" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// valid string | |||
param = fromJS({ | |||
required: false, | |||
type: "string", | |||
value: "test" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates required files", function() { | |||
// invalid file | |||
param = fromJS({ | |||
required: true, | |||
type: "file", | |||
@@ -193,9 +224,48 @@ describe("utils", function(){ | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// valid file | |||
param = fromJS({ | |||
required: true, | |||
type: "file", | |||
value: new win.File() | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates optional files", function() { | |||
// invalid file | |||
param = fromJS({ | |||
required: false, | |||
type: "file", | |||
value: "not a file" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Value must be a file"] ) | |||
// valid (empty) file | |||
param = fromJS({ | |||
required: false, | |||
type: "file", | |||
value: undefined | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// valid file | |||
param = fromJS({ | |||
required: false, | |||
type: "file", | |||
value: new win.File() | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates required arrays", function() { | |||
// invalid (empty) array | |||
param = fromJS({ | |||
required: true, | |||
type: "array", | |||
@@ -204,17 +274,191 @@ describe("utils", function(){ | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// invalid (not an array) | |||
param = fromJS({ | |||
required: true, | |||
type: "array", | |||
value: undefined | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// invalid array, items do not match correct type | |||
param = fromJS({ | |||
required: true, | |||
type: "array", | |||
value: [1], | |||
items: { | |||
type: "string" | |||
} | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [{index: 0, error: "Value must be a string"}] ) | |||
// valid array, with no 'type' for items | |||
param = fromJS({ | |||
required: true, | |||
type: "array", | |||
value: ["1"] | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// valid array, items match type | |||
param = fromJS({ | |||
required: true, | |||
type: "array", | |||
value: ["1"], | |||
items: { | |||
type: "string" | |||
} | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates optional arrays", function() { | |||
// valid, empty array | |||
param = fromJS({ | |||
required: false, | |||
type: "array", | |||
value: [] | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// invalid, items do not match correct type | |||
param = fromJS({ | |||
required: false, | |||
type: "array", | |||
value: ["number"], | |||
items: { | |||
type: "number" | |||
} | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [{index: 0, error: "Value must be a number"}] ) | |||
// valid | |||
param = fromJS({ | |||
required: false, | |||
type: "array", | |||
value: ["test"], | |||
items: { | |||
type: "string" | |||
} | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates required booleans", function() { | |||
// invalid boolean value | |||
param = fromJS({ | |||
required: true, | |||
type: "boolean", | |||
value: undefined | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// invalid boolean value (not a boolean) | |||
param = fromJS({ | |||
required: true, | |||
type: "boolean", | |||
value: "test string" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// valid boolean value | |||
param = fromJS({ | |||
required: true, | |||
type: "boolean", | |||
value: "true" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// valid boolean value | |||
param = fromJS({ | |||
required: true, | |||
type: "boolean", | |||
value: false | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates numbers", function() { | |||
// string instead of a number | |||
it("validates optional booleans", function() { | |||
// valid (empty) boolean value | |||
param = fromJS({ | |||
required: false, | |||
type: "boolean", | |||
value: undefined | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// invalid boolean value (not a boolean) | |||
param = fromJS({ | |||
required: false, | |||
type: "boolean", | |||
value: "test string" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Value must be a boolean"] ) | |||
// valid boolean value | |||
param = fromJS({ | |||
required: false, | |||
type: "boolean", | |||
value: "true" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// valid boolean value | |||
param = fromJS({ | |||
required: false, | |||
type: "boolean", | |||
value: false | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates required numbers", function() { | |||
// invalid number, string instead of a number | |||
param = fromJS({ | |||
required: true, | |||
type: "number", | |||
value: "test" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// invalid number, undefined value | |||
param = fromJS({ | |||
required: true, | |||
type: "number", | |||
value: undefined | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// valid number | |||
param = fromJS({ | |||
required: true, | |||
type: "number", | |||
value: 10 | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates optional numbers", function() { | |||
// invalid number, string instead of a number | |||
param = fromJS({ | |||
required: false, | |||
type: "number", | |||
@@ -223,7 +467,7 @@ describe("utils", function(){ | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Value must be a number"] ) | |||
// undefined value | |||
// valid (empty) number | |||
param = fromJS({ | |||
required: false, | |||
type: "number", | |||
@@ -232,78 +476,99 @@ describe("utils", function(){ | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
// null value | |||
// valid number | |||
param = fromJS({ | |||
required: false, | |||
type: "number", | |||
value: null | |||
value: 10 | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates integers", function() { | |||
// string instead of integer | |||
it("validates required integers", function() { | |||
// invalid integer, string instead of an integer | |||
param = fromJS({ | |||
required: false, | |||
required: true, | |||
type: "integer", | |||
value: "test" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( ["Value must be an integer"] ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// undefined value | |||
// invalid integer, undefined value | |||
param = fromJS({ | |||
required: false, | |||
required: true, | |||
type: "integer", | |||
value: undefined | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
expect( result ).toEqual( ["Required field is not provided"] ) | |||
// null value | |||
// valid integer | |||
param = fromJS({ | |||
required: false, | |||
required: true, | |||
type: "integer", | |||
value: null | |||
value: 10 | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
it("validates arrays", function() { | |||
// empty array | |||
it("validates optional integers", function() { | |||
// invalid integer, string instead of an integer | |||
param = fromJS({ | |||
required: false, | |||
type: "array", | |||
value: [] | |||
type: "integer", | |||
value: "test" | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [] ) | |||
expect( result ).toEqual( ["Value must be an integer"] ) | |||
// numbers | |||
// valid (empty) integer | |||
param = fromJS({ | |||
required: false, | |||
type: "array", | |||
value: ["number"], | |||
items: { | |||
type: "number" | |||
} | |||
type: "integer", | |||
value: undefined | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [{index: 0, error: "Value must be a number"}] ) | |||
expect( result ).toEqual( [] ) | |||
// integers | |||
// valid number | |||
param = fromJS({ | |||
required: false, | |||
type: "array", | |||
value: ["not", "numbers"], | |||
items: { | |||
type: "integer" | |||
} | |||
type: "integer", | |||
value: 10 | |||
}) | |||
result = validateParam( param, false ) | |||
expect( result ).toEqual( [{index: 0, error: "Value must be an integer"}, {index: 1, error: "Value must be an integer"}] ) | |||
expect( result ).toEqual( [] ) | |||
}) | |||
}) | |||
describe("fromJSOrdered", () => { | |||
it("should create an OrderedMap from an object", () => { | |||
const param = { | |||
value: "test" | |||
} | |||
const result = fromJSOrdered(param).toJS() | |||
expect( result ).toEqual( { value: "test" } ) | |||
}) | |||
it("should not use an object's length property for Map size", () => { | |||
const param = { | |||
length: 5 | |||
} | |||
const result = fromJSOrdered(param).toJS() | |||
expect( result ).toEqual( { length: 5 } ) | |||
}) | |||
it("should create an OrderedMap from an array", () => { | |||
const param = [1, 1, 2, 3, 5, 8] | |||
const result = fromJSOrdered(param).toJS() | |||
expect( result ).toEqual( [1, 1, 2, 3, 5, 8] ) | |||
}) | |||
}) | |||
}) |
@@ -1,64 +1,32 @@ | |||
var path = require('path') | |||
var rules = [ | |||
const path = require("path") | |||
const styleRules = require("./webpack.dist-style.config.js") | |||
let rules = [ | |||
{ test: /\.(worker\.js)(\?.*)?$/, | |||
use: [ | |||
{ | |||
loader: 'worker-loader', | |||
loader: "worker-loader", | |||
options: { | |||
inline: true, | |||
name: '[name].js' | |||
} | |||
}, | |||
{ loader: 'babel-loader' } | |||
] | |||
}, | |||
{ test: /\.(css)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
'postcss-loader' | |||
] | |||
}, | |||
{ test: /\.(scss)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
{ | |||
loader: 'postcss-loader', | |||
options: { sourceMap: true } | |||
}, | |||
{ loader: 'sass-loader', | |||
options: { | |||
outputStyle: 'expanded', | |||
sourceMap: true, | |||
sourceMapContents: 'true' | |||
name: "[name].js" | |||
} | |||
} | |||
] | |||
}, | |||
{ test: /\.(less)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
{ | |||
loader: 'postcss-loader', | |||
}, | |||
'less-loader' | |||
{ loader: "babel-loader" } | |||
] | |||
} | |||
] | |||
module.exports = require('./make-webpack-config.js')(rules, { | |||
module.exports = require("./make-webpack-config.js")(rules, { | |||
_special: { | |||
separateStylesheets: false, | |||
separateStylesheets: true, | |||
minimize: true, | |||
sourcemaps: true, | |||
}, | |||
entry: { | |||
'swagger-ui-bundle': [ | |||
'./src/polyfills', | |||
'./src/core/index.js' | |||
"swagger-ui-bundle": [ | |||
"./src/polyfills", | |||
"./src/core/index.js" | |||
] | |||
}, | |||
@@ -1,65 +1,32 @@ | |||
var path = require('path') | |||
const path = require("path") | |||
const styleRules = require("./webpack.dist-style.config.js") | |||
var rules = [ | |||
let rules = [ | |||
{ test: /\.(worker\.js)(\?.*)?$/, | |||
use: [ | |||
{ | |||
loader: 'worker-loader', | |||
loader: "worker-loader", | |||
options: { | |||
inline: true, | |||
name: '[name].js' | |||
name: "[name].js" | |||
} | |||
}, | |||
{ loader: 'babel-loader' } | |||
] | |||
}, | |||
{ test: /\.(css)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
'postcss-loader' | |||
] | |||
}, | |||
{ test: /\.(scss)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
{ | |||
loader: 'postcss-loader', | |||
options: { sourceMap: true } | |||
}, | |||
{ loader: 'sass-loader', | |||
options: { | |||
outputStyle: 'expanded', | |||
sourceMap: true, | |||
sourceMapContents: 'true' | |||
} | |||
} | |||
] | |||
}, | |||
{ test: /\.(less)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
{ | |||
loader: 'postcss-loader', | |||
}, | |||
'less-loader' | |||
{ loader: "babel-loader" } | |||
] | |||
} | |||
] | |||
module.exports = require('./make-webpack-config.js')(rules, { | |||
module.exports = require("./make-webpack-config.js")(rules, { | |||
_special: { | |||
separateStylesheets: false, | |||
separateStylesheets: true, | |||
minimize: true, | |||
sourcemaps: true, | |||
}, | |||
entry: { | |||
'swagger-ui-standalone-preset': [ | |||
'./src/polyfills', | |||
'./src/standalone/index.js' | |||
"swagger-ui-standalone-preset": [ | |||
"./src/polyfills", | |||
"./src/standalone/index.js" | |||
] | |||
}, | |||
@@ -1,66 +1,25 @@ | |||
var path = require('path') | |||
var fs = require('fs') | |||
const path = require("path") | |||
const fs = require("fs") | |||
const nodeModules = fs.readdirSync("node_modules").filter(function(x) { return x !== ".bin" }) | |||
var ExtractTextPlugin = require('extract-text-webpack-plugin') | |||
const styleRules = require("./webpack.dist-style.config.js") | |||
var rules = [ | |||
let rules = [ | |||
{ test: /\.(worker\.js)(\?.*)?$/, | |||
use: [ | |||
{ | |||
loader: 'worker-loader', | |||
loader: "worker-loader", | |||
options: { | |||
inline: true, | |||
name: '[name].js' | |||
name: "[name].js" | |||
} | |||
}, | |||
{ loader: 'babel-loader' } | |||
{ loader: "babel-loader" } | |||
] | |||
}, | |||
{ test: /\.(css)(\?.*)?$/, | |||
use: ExtractTextPlugin.extract({ | |||
fallback: 'style-loader', | |||
use: [ | |||
'css-loader', | |||
'postcss-loader' | |||
] | |||
}) | |||
}, | |||
{ test: /\.(scss)(\?.*)?$/, | |||
use: ExtractTextPlugin.extract({ | |||
fallback: 'style-loader', | |||
use: [ | |||
{ | |||
loader: 'css-loader', | |||
options: { minimize: true } | |||
}, | |||
{ | |||
loader: 'postcss-loader', | |||
options: { sourceMap: true } | |||
}, | |||
{ loader: 'sass-loader', | |||
options: { | |||
outputStyle: 'expanded', | |||
sourceMap: true, | |||
sourceMapContents: 'true' | |||
} | |||
} | |||
] | |||
}) | |||
}, | |||
{ test: /\.(less)(\?.*)?$/, | |||
use: ExtractTextPlugin.extract({ | |||
fallback: 'style-loader', | |||
use: ['css-loader', | |||
{ | |||
loader: 'postcss-loader', | |||
}, | |||
'less-loader' | |||
] | |||
}) | |||
} | |||
] | |||
rules = rules.concat(styleRules) | |||
module.exports = require('./make-webpack-config.js')(rules, { | |||
module.exports = require("./make-webpack-config.js")(rules, { | |||
_special: { | |||
separateStylesheets: true, | |||
minimize: true, | |||
@@ -1,56 +1,46 @@ | |||
var path = require('path') | |||
const path = require("path") | |||
var rules = [ | |||
const rules = [ | |||
{ test: /\.(worker\.js)(\?.*)?$/, | |||
use: [ | |||
{ | |||
loader: 'worker-loader', | |||
loader: "worker-loader", | |||
options: { | |||
inline: true | |||
} | |||
}, | |||
{ loader: 'babel-loader' } | |||
{ loader: "babel-loader" } | |||
] | |||
}, | |||
{ test: /\.(jsx)(\?.*)?$/, | |||
use: [ | |||
{ loader: 'react-hot-loader' }, | |||
{ loader: 'babel-loader' } | |||
{ loader: "react-hot-loader" }, | |||
{ loader: "babel-loader" } | |||
] | |||
}, | |||
{ test: /\.(css)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
'postcss-loader' | |||
"style-loader", | |||
"css-loader", | |||
"postcss-loader" | |||
] | |||
}, | |||
{ test: /\.(scss)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
"style-loader", | |||
"css-loader", | |||
{ | |||
loader: 'postcss-loader', | |||
loader: "postcss-loader", | |||
options: { sourceMap: true } | |||
}, | |||
{ loader: 'sass-loader', | |||
{ loader: "sass-loader", | |||
options: { | |||
outputStyle: 'expanded', | |||
outputStyle: "expanded", | |||
sourceMap: true, | |||
sourceMapContents: 'true' | |||
sourceMapContents: "true" | |||
} | |||
} | |||
] | |||
}, | |||
{ test: /\.(less)(\?.*)?$/, | |||
use: [ | |||
'style-loader', | |||
'css-loader', | |||
{ | |||
loader: 'postcss-loader', | |||
}, | |||
'less-loader' | |||
] | |||
} | |||
] | |||
@@ -60,25 +50,26 @@ module.exports = require("./make-webpack-config")(rules, { | |||
}, | |||
devtool: "eval", | |||
entry: { | |||
'swagger-ui-bundle': [ | |||
'./src/polyfills', | |||
'./src/core/index.js' | |||
"swagger-ui-bundle": [ | |||
"./src/polyfills", | |||
"./src/core/index.js" | |||
], | |||
'swagger-ui-standalone-preset': [ | |||
'./src/polyfills', | |||
'./src/standalone/index.js', | |||
"swagger-ui-standalone-preset": [ | |||
"./src/style/main.scss", | |||
"./src/polyfills", | |||
"./src/standalone/index.js", | |||
] | |||
}, | |||
output: { | |||
pathinfo: true, | |||
filename: '[name].js', | |||
filename: "[name].js", | |||
library: "[name]", | |||
libraryTarget: "umd", | |||
chunkFilename: "[id].js" | |||
}, | |||
devServer: { | |||
port: 3200, | |||
contentBase: path.join(__dirname, 'dev-helpers'), | |||
contentBase: path.join(__dirname, "dev-helpers"), | |||
publicPath: "/", | |||
noInfo: true, | |||
hot: true, | |||
@@ -1,3 +1,3 @@ | |||
var config = require("./webpack-dist.config.js") | |||
const config = require("./webpack-dist.config.js") | |||
module.exports = config |
@@ -1,8 +1,7 @@ | |||
const webpack = require('webpack') | |||
const path = require('path') | |||
const deepMerge = require('deepmerge') | |||
const webpackConfig = require('./webpack-dist-bundle.config.js') | |||
const DEPS_CHECK_DIR = require('./package.json').config.deps_check_dir | |||
const path = require("path") | |||
const deepMerge = require("deepmerge") | |||
const webpackConfig = require("./webpack-dist-bundle.config.js") | |||
const DEPS_CHECK_DIR = require("./package.json").config.deps_check_dir | |||
module.exports = deepMerge( | |||
webpackConfig, { | |||
@@ -1,3 +1,3 @@ | |||
module.exports = require("./make-webpack-config")({ | |||
}); | |||
}) |
@@ -0,0 +1,34 @@ | |||
const ExtractTextPlugin = require("extract-text-webpack-plugin") | |||
module.exports = [{ | |||
test: /\.(css)(\?.*)?$/, | |||
use: ExtractTextPlugin.extract({ | |||
fallback: "style-loader", | |||
use: [ | |||
"css-loader", | |||
"postcss-loader" | |||
] | |||
}) | |||
}, | |||
{ test: /\.(scss)(\?.*)?$/, | |||
use: ExtractTextPlugin.extract({ | |||
fallback: "style-loader", | |||
use: [ | |||
{ | |||
loader: "css-loader", | |||
options: { minimize: true } | |||
}, | |||
{ | |||
loader: "postcss-loader", | |||
options: { sourceMap: true } | |||
}, | |||
{ loader: "sass-loader", | |||
options: { | |||
outputStyle: "expanded", | |||
sourceMap: true, | |||
sourceMapContents: "true" | |||
} | |||
} | |||
] | |||
}) | |||
}] |