Merge branch 'backstage:master' into airbrake-initial
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Implement a `EntityTagsPicker` field extension
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-tech-insights': minor
|
||||
'@backstage/plugin-tech-insights': patch
|
||||
---
|
||||
|
||||
fixes api auth in tech-insights plugin
|
||||
Fixed API auth in tech-insights plugin
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Support custom file name for `catalog:write` action
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Filter out projects with missing `default_branch` from GitLab Discovery.
|
||||
@@ -78,3 +78,4 @@
|
||||
| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 |
|
||||
| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. |
|
||||
| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding |
|
||||
| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. |
|
||||
|
||||
@@ -53,8 +53,10 @@ To add a custom theme to your Backstage app, you pass it as configuration to
|
||||
For example, adding the theme that we created in the previous section can be
|
||||
done like this:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
import { createApp } from '@backstage/app-defaults';
|
||||
import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
@@ -63,7 +65,11 @@ const app = createApp({
|
||||
id: 'my-theme',
|
||||
title: 'My Custom Theme',
|
||||
variant: 'light',
|
||||
theme: myTheme,
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={myTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
),
|
||||
}]
|
||||
})
|
||||
```
|
||||
@@ -76,60 +82,67 @@ want to use the default themes, they are exported as `lightTheme` and
|
||||
## Example of a custom theme
|
||||
|
||||
```ts
|
||||
const themeOptions = createThemeOptions({
|
||||
import {
|
||||
createTheme,
|
||||
genPageTheme,
|
||||
lightTheme,
|
||||
shapes,
|
||||
} from '@backstage/theme';
|
||||
|
||||
const myTheme = createTheme({
|
||||
palette: {
|
||||
...lightTheme.palette,
|
||||
primary: {
|
||||
main: '#123456',
|
||||
main: '#343b58',
|
||||
},
|
||||
secondary: {
|
||||
main: '#123456',
|
||||
main: '#565a6e',
|
||||
},
|
||||
error: {
|
||||
main: '#123456',
|
||||
main: '#8c4351',
|
||||
},
|
||||
warning: {
|
||||
main: '#123456',
|
||||
main: '#8f5e15',
|
||||
},
|
||||
info: {
|
||||
main: '#123456',
|
||||
main: '#34548a',
|
||||
},
|
||||
success: {
|
||||
main: '#123456',
|
||||
main: '#485e30',
|
||||
},
|
||||
background: {
|
||||
default: '#123456',
|
||||
paper: '#123456',
|
||||
default: '#d5d6db',
|
||||
paper: '#d5d6db',
|
||||
},
|
||||
banner: {
|
||||
info: '#123456',
|
||||
error: '#123456',
|
||||
text: '#123456',
|
||||
link: '#123456',
|
||||
info: '#34548a',
|
||||
error: '#8c4351',
|
||||
text: '#343b58',
|
||||
link: '#565a6e',
|
||||
},
|
||||
errorBackground: '#123456',
|
||||
warningBackground: '#123456',
|
||||
infoBackground: '#123456',
|
||||
errorBackground: '#8c4351',
|
||||
warningBackground: '#8f5e15',
|
||||
infoBackground: '#343b58',
|
||||
navigation: {
|
||||
background: '#123456',
|
||||
indicator: '#123456',
|
||||
color: '#123456',
|
||||
selectedColor: '#123456',
|
||||
background: '#343b58',
|
||||
indicator: '#8f5e15',
|
||||
color: '#d5d6db',
|
||||
selectedColor: '#ffffff',
|
||||
},
|
||||
},
|
||||
defaultPageTheme: 'home',
|
||||
fontFamily: 'Comic Sans',
|
||||
fontFamily: 'Comic Sans MS',
|
||||
/* below drives the header colors */
|
||||
pageTheme: {
|
||||
home: genPageTheme(['#123456', '#123456'], shapes.wave),
|
||||
documentation: genPageTheme(['#123456', '#123456'], shapes.wave2),
|
||||
tool: genPageTheme(['#123456', '#123456'], shapes.round),
|
||||
service: genPageTheme(['#123456', '#123456'], shapes.wave),
|
||||
website: genPageTheme(['#123456', '#123456'], shapes.wave),
|
||||
library: genPageTheme(['#123456', '#123456'], shapes.wave),
|
||||
other: genPageTheme(['#123456', '#123456'], shapes.wave),
|
||||
app: genPageTheme(['#123456', '#123456'], shapes.wave),
|
||||
apis: genPageTheme(['#123456', '#123456'], shapes.wave),
|
||||
home: genPageTheme(['#8c4351', '#343b58'], shapes.wave),
|
||||
documentation: genPageTheme(['#8c4351', '#343b58'], shapes.wave2),
|
||||
tool: genPageTheme(['#8c4351', '#343b58'], shapes.round),
|
||||
service: genPageTheme(['#8c4351', '#343b58'], shapes.wave),
|
||||
website: genPageTheme(['#8c4351', '#343b58'], shapes.wave),
|
||||
library: genPageTheme(['#8c4351', '#343b58'], shapes.wave),
|
||||
other: genPageTheme(['#8c4351', '#343b58'], shapes.wave),
|
||||
app: genPageTheme(['#8c4351', '#343b58'], shapes.wave),
|
||||
apis: genPageTheme(['#8c4351', '#343b58'], shapes.wave),
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -162,7 +175,7 @@ wouldn't be enough to alter the `box-shadow` property or to add css rules that
|
||||
aren't already defined like a margin. For these cases you should also create an
|
||||
override.
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
import { createApp } from '@backstage/core-app-api';
|
||||
import { BackstageTheme, lightTheme } from '@backstage/theme';
|
||||
/**
|
||||
@@ -187,6 +200,16 @@ export const createCustomThemeOverrides = (
|
||||
};
|
||||
};
|
||||
|
||||
const customTheme: BackstageTheme = {
|
||||
...lightTheme,
|
||||
overrides: {
|
||||
// These are the overrides that Backstage applies to `material-ui` components
|
||||
...lightTheme.overrides,
|
||||
// These are your custom overrides, either to `material-ui` or Backstage components.
|
||||
...createCustomThemeOverrides(lightTheme),
|
||||
},
|
||||
};
|
||||
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
@@ -194,15 +217,11 @@ const app = createApp({
|
||||
id: 'my-theme',
|
||||
title: 'My Custom Theme',
|
||||
variant: 'light',
|
||||
theme: {
|
||||
...lightTheme,
|
||||
overrides: {
|
||||
// These are the overrides that Backstage applies to `material-ui` components
|
||||
...lightTheme.overrides,
|
||||
// These are your custom overrides, either to `material-ui` or Backstage components.
|
||||
...createCustomThemeOverrides(lightTheme),
|
||||
},
|
||||
},
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={customTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
),
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
@@ -184,6 +184,13 @@ describe('GitlabDiscoveryProcessor', () => {
|
||||
last_activity_at: '2021-08-05T11:03:05.774Z',
|
||||
web_url: 'https://gitlab.fake/3',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
archived: false,
|
||||
default_branch: undefined, // MISSING DEFAULT BRANCH
|
||||
last_activity_at: '2021-08-05T11:03:05.774Z',
|
||||
web_url: 'https://gitlab.fake/4',
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
|
||||
@@ -94,9 +94,16 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor {
|
||||
};
|
||||
for await (const project of projects) {
|
||||
result.scanned++;
|
||||
if (!project.archived) {
|
||||
result.matches.push(project);
|
||||
|
||||
if (project.archived) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (branch === '*' && project.default_branch === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.matches.push(project);
|
||||
}
|
||||
|
||||
for (const project of result.matches) {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
export type GitLabProject = {
|
||||
id: number;
|
||||
default_branch: string;
|
||||
default_branch?: string;
|
||||
archived: boolean;
|
||||
last_activity_at: string;
|
||||
web_url: string;
|
||||
|
||||
@@ -69,4 +69,33 @@ describe('catalog:write', () => {
|
||||
yaml.stringify(entity),
|
||||
);
|
||||
});
|
||||
|
||||
it('should support a custom filename', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'n',
|
||||
namespace: 'ns',
|
||||
annotations: {
|
||||
[ORIGIN_LOCATION_ANNOTATION]: 'url:https://example.com',
|
||||
},
|
||||
},
|
||||
spec: {},
|
||||
};
|
||||
|
||||
await action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
filePath: 'some-dir/entity-info.yaml',
|
||||
entity,
|
||||
},
|
||||
});
|
||||
|
||||
expect(fsMock.writeFile).toHaveBeenCalledTimes(1);
|
||||
expect(fsMock.writeFile).toHaveBeenCalledWith(
|
||||
resolvePath(mockContext.workspacePath, 'some-dir/entity-info.yaml'),
|
||||
yaml.stringify(entity),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,13 +21,18 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { resolveSafeChildPath } from '@backstage/backend-common';
|
||||
|
||||
export function createCatalogWriteAction() {
|
||||
return createTemplateAction<{ name?: string; entity: Entity }>({
|
||||
return createTemplateAction<{ filePath?: string; entity: Entity }>({
|
||||
id: 'catalog:write',
|
||||
description: 'Writes the catalog-info.yaml for your template',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
filePath: {
|
||||
title: 'Catalog file path',
|
||||
description: 'Defaults to catalog-info.yaml',
|
||||
type: 'string',
|
||||
},
|
||||
entity: {
|
||||
title: 'Entity info to write catalog-info.yaml',
|
||||
description:
|
||||
@@ -39,10 +44,11 @@ export function createCatalogWriteAction() {
|
||||
},
|
||||
async handler(ctx) {
|
||||
ctx.logStream.write(`Writing catalog-info.yaml`);
|
||||
const { entity } = ctx.input;
|
||||
const { filePath, entity } = ctx.input;
|
||||
const path = filePath ?? 'catalog-info.yaml';
|
||||
|
||||
await fs.writeFile(
|
||||
resolveSafeChildPath(ctx.workspacePath, 'catalog-info.yaml'),
|
||||
resolveSafeChildPath(ctx.workspacePath, path),
|
||||
yaml.stringify(entity),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -80,6 +80,16 @@ export const EntityPicker: ({
|
||||
// @public (undocumented)
|
||||
export const EntityPickerFieldExtension: () => null;
|
||||
|
||||
// @public
|
||||
export const EntityTagsPicker: ({
|
||||
formData,
|
||||
onChange,
|
||||
uiSchema,
|
||||
}: FieldProps<string[]>) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const EntityTagsPickerFieldExtension: () => null;
|
||||
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "FavouriteTemplate" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { useAsync, useEffectOnce } from 'react-use';
|
||||
import { CatalogEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { Entity, makeValidator } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { FormControl, TextField } from '@material-ui/core';
|
||||
import { Autocomplete } from '@material-ui/lab';
|
||||
import { FieldProps } from '@rjsf/core';
|
||||
|
||||
/**
|
||||
* EntityTagsPicker
|
||||
* @public
|
||||
*/
|
||||
export const EntityTagsPicker = ({
|
||||
formData,
|
||||
onChange,
|
||||
uiSchema,
|
||||
}: FieldProps<string[]>) => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputError, setInputError] = useState(false);
|
||||
const tagValidator = makeValidator().isValidTag;
|
||||
const kinds = uiSchema['ui:options']?.kinds as string[];
|
||||
|
||||
const { loading, value: existingTags } = useAsync(async () => {
|
||||
const tagsRequest: CatalogEntitiesRequest = { fields: ['metadata.tags'] };
|
||||
if (kinds) {
|
||||
tagsRequest.filter = { kind: kinds };
|
||||
}
|
||||
|
||||
const entities = await catalogApi.getEntities(tagsRequest);
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
entities.items
|
||||
.flatMap((e: Entity) => e.metadata?.tags)
|
||||
.filter(Boolean) as string[],
|
||||
),
|
||||
].sort();
|
||||
});
|
||||
|
||||
const setTags = (_: React.ChangeEvent<{}>, values: string[] | null) => {
|
||||
// Reset error state in case all tags were removed
|
||||
let hasError = false;
|
||||
let addDuplicate = false;
|
||||
const currentTags = formData || [];
|
||||
|
||||
// If adding a new tag
|
||||
if (values?.length && currentTags.length < values.length) {
|
||||
const newTag = (values[values.length - 1] = values[values.length - 1]
|
||||
.toLocaleLowerCase('en-US')
|
||||
.trim());
|
||||
hasError = !tagValidator(newTag);
|
||||
addDuplicate = currentTags.indexOf(newTag) !== -1;
|
||||
}
|
||||
|
||||
setInputError(hasError);
|
||||
setInputValue(!hasError ? '' : inputValue);
|
||||
if (!hasError && !addDuplicate) {
|
||||
onChange(values || []);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize field to always return an array
|
||||
useEffectOnce(() => onChange(formData || []));
|
||||
|
||||
return (
|
||||
<FormControl margin="normal">
|
||||
<Autocomplete
|
||||
multiple
|
||||
freeSolo
|
||||
filterSelectedOptions
|
||||
onChange={setTags}
|
||||
value={formData || []}
|
||||
inputValue={inputValue}
|
||||
loading={loading}
|
||||
options={existingTags || []}
|
||||
ChipProps={{ size: 'small' }}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Tags"
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
error={inputError}
|
||||
helperText="Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { EntityTagsPicker } from './EntityTagsPicker';
|
||||
@@ -19,3 +19,4 @@ export * from './OwnerPicker';
|
||||
export * from './RepoUrlPicker';
|
||||
export * from './TextValuePicker';
|
||||
export * from './OwnedEntityPicker';
|
||||
export * from './EntityTagsPicker';
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
EntityNamePicker,
|
||||
entityNamePickerValidation,
|
||||
} from '../components/fields/EntityNamePicker';
|
||||
import { EntityTagsPicker } from '../components/fields/EntityTagsPicker';
|
||||
import { OwnerPicker } from '../components/fields/OwnerPicker';
|
||||
import {
|
||||
repoPickerValidation,
|
||||
@@ -36,6 +37,10 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [
|
||||
name: 'EntityNamePicker',
|
||||
validation: entityNamePickerValidation,
|
||||
},
|
||||
{
|
||||
component: EntityTagsPicker,
|
||||
name: 'EntityTagsPicker',
|
||||
},
|
||||
{
|
||||
component: RepoUrlPicker,
|
||||
name: 'RepoUrlPicker',
|
||||
|
||||
@@ -30,6 +30,7 @@ export type { CustomFieldValidator, FieldExtensionOptions } from './extensions';
|
||||
export {
|
||||
EntityPickerFieldExtension,
|
||||
EntityNamePickerFieldExtension,
|
||||
EntityTagsPickerFieldExtension,
|
||||
OwnerPickerFieldExtension,
|
||||
OwnedEntityPickerFieldExtension,
|
||||
RepoUrlPickerFieldExtension,
|
||||
@@ -40,6 +41,7 @@ export {
|
||||
export {
|
||||
EntityNamePicker,
|
||||
EntityPicker,
|
||||
EntityTagsPicker,
|
||||
OwnerPicker,
|
||||
RepoUrlPicker,
|
||||
TextValuePicker,
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker';
|
||||
import { EntityTagsPicker } from './components/fields/EntityTagsPicker';
|
||||
|
||||
export const scaffolderPlugin = createPlugin({
|
||||
id: 'scaffolder',
|
||||
@@ -103,3 +104,14 @@ export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide(
|
||||
name: 'OwnedEntityPicker',
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* EntityTagsPickerFieldExtension
|
||||
* @public
|
||||
*/
|
||||
export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
component: EntityTagsPicker,
|
||||
name: 'EntityTagsPicker',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -7839,9 +7839,9 @@
|
||||
integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
|
||||
|
||||
"@types/jscodeshift@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.npmjs.org/@types/jscodeshift/-/jscodeshift-0.11.0.tgz#7224cf1a4d0383b4fb2694ffed52f57b45c3325b"
|
||||
integrity sha512-OcJgr5GznWCEx2Oeg4eMUZYwssTHFj/tU8grrNCKdFQtAEAa0ezDiPHbCdSkyWrRSurXrYbNbHdhxbbB76pXNg==
|
||||
version "0.11.3"
|
||||
resolved "https://registry.npmjs.org/@types/jscodeshift/-/jscodeshift-0.11.3.tgz#8dcab24ced39dcab1c8ff3461b3d171aafee3d48"
|
||||
integrity sha512-pM0JD9kWVDH9DQp5Y6td16924V3MwZHei8P3cTeuFhXpzpk0K+iWraBZz8wF61QkFs9fZeAQNX0q8SG0+TFm2w==
|
||||
dependencies:
|
||||
ast-types "^0.14.1"
|
||||
recast "^0.20.3"
|
||||
@@ -14864,9 +14864,9 @@ express-prom-bundle@^6.3.6:
|
||||
url-value-parser "^2.0.0"
|
||||
|
||||
express-promise-router@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-4.1.0.tgz#79160e145c27610ba411bceb0552a36f11dbab4f"
|
||||
integrity sha512-nvg0X1Rj8oajPPC+fG3t4e740aNmQZRZY6dRLbiiM56Dvd8213RJ4kaxhZVTdQLut+l4DZdfeJkyx2VENPMBdw==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-4.1.1.tgz#8fac102060b9bcc868f84d34fbb12fd8fa494291"
|
||||
integrity sha512-Lkvcy/ZGrBhzkl3y7uYBHLMtLI4D6XQ2kiFg9dq7fbktBch5gjqJ0+KovX0cvCAvTJw92raWunRLM/OM+5l4fA==
|
||||
dependencies:
|
||||
is-promise "^4.0.0"
|
||||
lodash.flattendeep "^4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user