Merge remote-tracking branch 'refs/remotes/origin/master' into gauge-with-decimal-digits
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Internal updates to allow reusing Backstage's `fetchApi` implementation for event source requests. This allows you to for example, override the `Authorization` header.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend-module-oidc-provider': patch
|
||||
---
|
||||
|
||||
if oidc server do not provide revocation_endpoint,we should not call revoke function
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Removed max width from `Select` component.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Updated `node-gyp` to v10
|
||||
@@ -270,3 +270,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
|
||||
| [OP Financial Group](https://www.op.fi/op-financial-group) | [Heikki Hellgren](https://github.com/drodil), [Jyrki Koistinen](https://github.com/snyvision) | We are using Backstage as a gateway into our internal development platform offering to simplify complexity. |
|
||||
| [Scania](https://www.scania.com) | [Santosh Rangavajjula](https://linkedin.com/in/rsbth) | We are implementing backstage at Scania to consolidate operational information from Gitlab, Jira, Confluence, Artifactory, Servicenow and other tools into one place. |
|
||||
| [Senora.dev](https://senora.dev) | [Shaked Braimok Yosef](https://github.com/ShakedBraimok) | We are using Backstage as a service catalog for our costumers. |
|
||||
| [Covestro](https://www.covestro.com) | [Julien Gedeon](https://github.com/cvvcv1) | We use Backstage as our Internal developer portal for the Digital R&D department. |
|
||||
|
||||
@@ -27,6 +27,45 @@ any configuration. They generate self-signed tokens automatically for making
|
||||
requests to other Backstage backend plugins, and the receivers use the caller's
|
||||
public key set endpoint to be able to perform verification.
|
||||
|
||||
A backend plugin wishing to make a request to another backend plugin acquires
|
||||
the required token as follows, where `auth` and `httpAuth` are assumed to be
|
||||
injected from `coreServices.auth` and `coreServices.httpAuth`, respectively:
|
||||
|
||||
```ts
|
||||
const credentials = await httpAuth.credentials(req);
|
||||
const { token } = await auth.getPluginRequestToken({
|
||||
onBehalfOf: credentials,
|
||||
targetPluginId: '<plugin-id>', // e.g. 'catalog'
|
||||
});
|
||||
```
|
||||
|
||||
In this example we are assuming that we are in an Express request handler, and
|
||||
we extract the caller credentials (typically a user or a service) out of its
|
||||
`req` and make the upstream request on-behalf-of that principal. Prefer to use
|
||||
this pattern wherever there's an incoming set of credentials to refer to.
|
||||
|
||||
If you want to initiate a request entirely as your own service, not on behalf of
|
||||
anybody else, you can do so as follows:
|
||||
|
||||
```ts
|
||||
const { token } = await auth.getPluginRequestToken({
|
||||
onBehalfOf: await auth.getOwnServiceCredentials(),
|
||||
targetPluginId: '<plugin-id>', // e.g. 'catalog'
|
||||
});
|
||||
```
|
||||
|
||||
Callers pass along the tokens verbatim with requests in the `Authorization`
|
||||
header:
|
||||
|
||||
```yaml
|
||||
Authorization: Bearer eyJhbG...
|
||||
```
|
||||
|
||||
You may occasionally also see some code, e.g. clients to other systems, that
|
||||
accept a `credentials` argument directly instead of a token. For those, just
|
||||
pass in the credentials as acquired above, instead of making a token. The client
|
||||
code will know what to do with those credentials internally.
|
||||
|
||||
This flow has only one configuration option to set in your app-config:
|
||||
`backend.auth.dangerouslyDisableDefaultAuthPolicy`, which can be set to `true`
|
||||
if you for some reason need to completely disable both the issuing and
|
||||
@@ -73,8 +112,8 @@ The subjects must be strings without whitespace. They are used for identifying
|
||||
each caller, and become part of the credentials object that request recipient
|
||||
plugins get.
|
||||
|
||||
Callers pass along the tokens verbatim with requests in the `Authorization`
|
||||
header:
|
||||
Callers must pass along tokens verbatim with requests in the `Authorization`
|
||||
header when calling Backstage plugins:
|
||||
|
||||
```yaml
|
||||
Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW
|
||||
@@ -125,6 +164,13 @@ credentials object that the request recipient plugins get. All subjects will hav
|
||||
`external:`, but you can also provide a custom subjectPrefix which will get appended before the
|
||||
subject returned from your JWKS service (ex. `external:custom-prefix:sub`).
|
||||
|
||||
Callers must pass along tokens with requests in the `Authorization` header when
|
||||
calling Backstage plugins:
|
||||
|
||||
```yaml
|
||||
Authorization: Bearer eyJhbG...
|
||||
```
|
||||
|
||||
## Legacy Tokens
|
||||
|
||||
Plugins and backends that are _not_ on the new backend system use a legacy token
|
||||
|
||||
@@ -19,7 +19,11 @@ The diagram below provides an overview of the different building blocks, and the
|
||||
|
||||

|
||||
|
||||
> NOTE: These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns.
|
||||
:::note Note
|
||||
|
||||
These are all concepts that existed in our old backend system in one way or another, but they have now all been lifted up to be first class concerns.
|
||||
|
||||
:::
|
||||
|
||||
### Backend
|
||||
|
||||
|
||||
@@ -192,7 +192,11 @@ When defining a default factory for a service, it is possible for it to end up w
|
||||
|
||||
## Service Factory Options
|
||||
|
||||
> NOTE: This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead.
|
||||
:::note Note
|
||||
|
||||
This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead.
|
||||
|
||||
:::
|
||||
|
||||
When declaring a service factory it's possible to include an options callback. This allows you to customize the factory through code when installing it in the backend. For example, this is how you install an explicit factory instance in the backend without any options:
|
||||
|
||||
|
||||
@@ -6,8 +6,12 @@ sidebar_label: Overview
|
||||
description: Building backend plugins and modules using the new backend system
|
||||
---
|
||||
|
||||
> NOTE: If you have an existing backend and/or backend plugins that are not yet
|
||||
> using the new backend system, see [migrating](./08-migrating.md).
|
||||
:::note Note
|
||||
|
||||
If you have an existing backend and/or backend plugins that are not yet
|
||||
using the new backend system, see [migrating](./08-migrating.md).
|
||||
|
||||
:::
|
||||
|
||||
This section covers how to build your own backend [plugins](../architecture/04-plugins.md) and
|
||||
[modules](../architecture/06-modules.md). They are sometimes collectively referred to as
|
||||
|
||||
@@ -10,7 +10,11 @@ description: Testing plugins in the frontend system
|
||||
|
||||
# Testing Frontend Plugins
|
||||
|
||||
> NOTE: The new frontend system is in alpha, and some plugins do not yet fully implement it.
|
||||
:::note Note
|
||||
|
||||
The new frontend system is in alpha, and some plugins do not yet fully implement it.
|
||||
|
||||
:::
|
||||
|
||||
Utilities for testing frontend features and components are available in `@backstage/frontend-test-utils`.
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: InfraWallet
|
||||
author: Electrolux Group
|
||||
authorUrl: https://opensource.electrolux.one/
|
||||
category: FinOps
|
||||
description: Control your cloud costs just in the way how you control your bank accounts.
|
||||
documentation: https://github.com/electrolux-oss/infrawallet/blob/main/README.md
|
||||
iconUrl: /img/infrawallet-logo.png
|
||||
npmPackageName: '@electrolux-oss/plugin-infrawallet'
|
||||
tags:
|
||||
- finops
|
||||
- cost
|
||||
- infrastructure
|
||||
addedDate: '2024-06-03'
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -78,7 +78,6 @@ const useStyles = makeStyles(
|
||||
createStyles({
|
||||
formControl: {
|
||||
margin: theme.spacing(1, 0),
|
||||
maxWidth: 300,
|
||||
},
|
||||
label: {
|
||||
transform: 'initial',
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@spotify/prettier-config": "^12.0.0",
|
||||
"concurrently": "^8.0.0",
|
||||
"lerna": "^7.3.0",
|
||||
"node-gyp": "^9.0.0",
|
||||
"node-gyp": "^10.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"typescript": "~5.4.0"
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"app": "link:../app",
|
||||
"better-sqlite3": "^9.0.0",
|
||||
"dockerode": "^3.3.1",
|
||||
"node-gyp": "^9.0.0",
|
||||
"node-gyp": "^10.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
|
||||
@@ -493,5 +493,52 @@ describe('oidcAuthenticator', () => {
|
||||
new Error('Refresh failed'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not revoke refreshToken when issuer revocation_endpoint is undefined', async () => {
|
||||
const refreshToken = 'revokeRefreshToken2';
|
||||
const refreshRequest = {
|
||||
scope: 'testScope',
|
||||
refreshToken,
|
||||
req: {} as express.Request,
|
||||
};
|
||||
const logoutRequest = {
|
||||
refreshToken,
|
||||
req: {} as express.Request,
|
||||
};
|
||||
|
||||
// override .well-known endpoint response, set revocation_endpoint to undefined
|
||||
mswServer.use(
|
||||
rest.get(
|
||||
'https://oidc.test/.well-known/openid-configuration',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json({
|
||||
...issuerMetadata,
|
||||
revocation_endpoint: undefined,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const newImplementation = oidcAuthenticator.initialize({
|
||||
callbackUrl: 'https://backstage.test/callback',
|
||||
config: new ConfigReader({
|
||||
metadataUrl: 'https://oidc.test/.well-known/openid-configuration',
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
}),
|
||||
});
|
||||
|
||||
await oidcAuthenticator.logout?.(logoutRequest, newImplementation);
|
||||
|
||||
const refreshResponse = await oidcAuthenticator.refresh(
|
||||
refreshRequest,
|
||||
newImplementation,
|
||||
);
|
||||
|
||||
expect(refreshResponse.session.refreshToken).toBe('refreshToken');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,8 +202,15 @@ export const oidcAuthenticator = createOAuthAuthenticator({
|
||||
|
||||
async logout(input, ctx) {
|
||||
const { client } = await ctx.promise;
|
||||
const issuer = client.issuer;
|
||||
/**
|
||||
* https://github.com/panva/node-openid-client/blob/main/lib/client.js#L1398
|
||||
* client.revoke will check revocation_endpoint and if undefined throw error。
|
||||
*
|
||||
* if oidc server do not provide revocation_endpoint,we should not call revoke function
|
||||
*/
|
||||
|
||||
if (input.refreshToken) {
|
||||
if (input.refreshToken && issuer.revocation_endpoint) {
|
||||
await client.revoke(input.refreshToken);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@react-hookz/web": "^24.0.0",
|
||||
"@rjsf/core": "5.18.2",
|
||||
"@rjsf/material-ui": "5.18.2",
|
||||
@@ -78,7 +79,6 @@
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"@uiw/react-codemirror": "^4.9.3",
|
||||
"classnames": "^2.2.6",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"humanize-duration": "^3.25.1",
|
||||
"json-schema": "^0.4.0",
|
||||
|
||||
@@ -20,14 +20,13 @@ import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { ScaffolderClient } from './api';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
|
||||
const MockedEventSource = EventSourcePolyfill as jest.MockedClass<
|
||||
typeof EventSourcePolyfill
|
||||
jest.mock('@microsoft/fetch-event-source');
|
||||
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
|
||||
typeof fetchEventSource
|
||||
>;
|
||||
|
||||
jest.mock('event-source-polyfill');
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
describe('api', () => {
|
||||
@@ -87,26 +86,23 @@ describe('api', () => {
|
||||
describe('streamEvents', () => {
|
||||
describe('eventsource', () => {
|
||||
it('should work', async () => {
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'log') {
|
||||
fn({
|
||||
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
|
||||
} as any);
|
||||
} else if (type === 'completion') {
|
||||
fn({
|
||||
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
|
||||
} as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const token = 'fake-token';
|
||||
identityApi.getCredentials.mockResolvedValue({ token: token });
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({
|
||||
id: '',
|
||||
event: 'log',
|
||||
data: '{"id":1,"taskId":"a-random-id","type":"log","createdAt":"","body":{"message":"My log message"}}',
|
||||
});
|
||||
await Promise.resolve();
|
||||
onmessage?.({
|
||||
id: '',
|
||||
event: 'completion',
|
||||
data: '{"id":2,"taskId":"a-random-id","type":"completion","createdAt":"","body":{"message":"Finished!"}}',
|
||||
});
|
||||
});
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
@@ -116,14 +112,15 @@ describe('api', () => {
|
||||
.subscribe({ next, complete });
|
||||
});
|
||||
|
||||
expect(MockedEventSource).toHaveBeenCalledWith(
|
||||
expect(mockFetchEventSource).toHaveBeenCalledWith(
|
||||
'http://backstage/api/v2/tasks/a-random-task-id/eventstream',
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
fetch: fetchApi.fetch,
|
||||
onmessage: expect.any(Function),
|
||||
onerror: expect.any(Function),
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
expect(MockedEventSource.prototype.close).toHaveBeenCalled();
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(2);
|
||||
expect(next).toHaveBeenCalledWith({
|
||||
|
||||
@@ -41,7 +41,10 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-react';
|
||||
|
||||
import queryString from 'qs';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import {
|
||||
EventSourceMessage,
|
||||
fetchEventSource,
|
||||
} from '@microsoft/fetch-event-source';
|
||||
|
||||
/**
|
||||
* An API to interact with the scaffolder backend.
|
||||
@@ -226,11 +229,8 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
params.set('after', String(Number(after)));
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
this.discoveryApi.getBaseUrl('scaffolder'),
|
||||
this.identityApi?.getCredentials(),
|
||||
]).then(
|
||||
([baseUrl, credentials]) => {
|
||||
this.discoveryApi.getBaseUrl('scaffolder').then(
|
||||
baseUrl => {
|
||||
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
|
||||
taskId,
|
||||
)}/eventstream`;
|
||||
@@ -245,22 +245,25 @@ export class ScaffolderClient implements ScaffolderApi {
|
||||
}
|
||||
};
|
||||
|
||||
const eventSource = new EventSourcePolyfill(url, {
|
||||
withCredentials: true,
|
||||
headers: credentials?.token
|
||||
? { Authorization: `Bearer ${credentials.token}` }
|
||||
: {},
|
||||
});
|
||||
eventSource.addEventListener('log', processEvent);
|
||||
eventSource.addEventListener('recovered', processEvent);
|
||||
eventSource.addEventListener('cancelled', processEvent);
|
||||
eventSource.addEventListener('completion', (event: any) => {
|
||||
processEvent(event);
|
||||
eventSource.close();
|
||||
subscriber.complete();
|
||||
});
|
||||
eventSource.addEventListener('error', event => {
|
||||
subscriber.error(event);
|
||||
const ctrl = new AbortController();
|
||||
fetchEventSource(url, {
|
||||
fetch: this.fetchApi.fetch,
|
||||
signal: ctrl.signal,
|
||||
onmessage(e: EventSourceMessage) {
|
||||
if (e.event === 'log') {
|
||||
processEvent(e);
|
||||
return;
|
||||
} else if (e.event === 'completion') {
|
||||
processEvent(e);
|
||||
subscriber.complete();
|
||||
ctrl.abort();
|
||||
return;
|
||||
}
|
||||
processEvent(e);
|
||||
},
|
||||
onerror(err) {
|
||||
subscriber.error(err);
|
||||
},
|
||||
});
|
||||
},
|
||||
error => {
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const { EventSourcePolyfill } = jest.requireMock('event-source-polyfill');
|
||||
global.EventSource = EventSourcePolyfill;
|
||||
|
||||
// Patch jsdom to add feature used by CodeMirror
|
||||
document.createRange = () => {
|
||||
const range = new Range();
|
||||
|
||||
@@ -465,8 +465,8 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 {
|
||||
constructor(options: {
|
||||
configApi: Config;
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
fetchApi: FetchApi;
|
||||
identityApi?: IdentityApi;
|
||||
});
|
||||
// (undocumented)
|
||||
configApi: Config;
|
||||
@@ -485,8 +485,6 @@ export class TechDocsStorageClient implements TechDocsStorageApi_2 {
|
||||
getEntityDocs(entityId: CompoundEntityRef, path: string): Promise<string>;
|
||||
// (undocumented)
|
||||
getStorageUrl(): Promise<string>;
|
||||
// (undocumented)
|
||||
identityApi: IdentityApi;
|
||||
syncEntityDocs(
|
||||
entityId: CompoundEntityRef,
|
||||
logHandler?: (line: string) => void,
|
||||
|
||||
@@ -25,11 +25,7 @@ import {
|
||||
techdocsStorageApiRef,
|
||||
} from '../src';
|
||||
|
||||
import {
|
||||
configApiRef,
|
||||
discoveryApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { configApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
import { TechDocsReaderPageProvider } from '@backstage/plugin-techdocs-react';
|
||||
import { Header, Page, TabbedLayout } from '@backstage/core-components';
|
||||
|
||||
@@ -135,7 +131,6 @@ createDevApp()
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
},
|
||||
factory: () => apiBridge,
|
||||
})
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.10.0",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"dompurify": "^3.0.0",
|
||||
"event-source-polyfill": "1.0.25",
|
||||
"git-url-parse": "^14.0.0",
|
||||
"jss": "~10.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -90,7 +90,6 @@
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/dompurify": "^3.0.0",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"canvas": "^2.10.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
compatWrapper,
|
||||
@@ -55,14 +54,12 @@ const techDocsStorageApi = createApiExtension({
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
|
||||
factory: ({ configApi, discoveryApi, fetchApi }) =>
|
||||
new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -17,16 +17,15 @@
|
||||
import { UrlPatternDiscovery } from '@backstage/core-app-api';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import { MockConfigApi, MockFetchApi } from '@backstage/test-utils';
|
||||
import { TechDocsStorageClient } from './client';
|
||||
|
||||
const MockedEventSource = EventSourcePolyfill as jest.MockedClass<
|
||||
typeof EventSourcePolyfill
|
||||
jest.mock('@microsoft/fetch-event-source');
|
||||
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
|
||||
typeof fetchEventSource
|
||||
>;
|
||||
|
||||
jest.mock('event-source-polyfill');
|
||||
|
||||
const mockEntity = {
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
@@ -51,7 +50,6 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
@@ -72,7 +70,6 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
@@ -84,71 +81,50 @@ describe('TechDocsStorageClient', () => {
|
||||
});
|
||||
|
||||
describe('syncEntityDocs', () => {
|
||||
it('should create eventsource without headers', async () => {
|
||||
it('should create eventsource with fetch', async () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
|
||||
});
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
await storageApi.syncEntityDocs(mockEntity);
|
||||
|
||||
expect(MockedEventSource).toHaveBeenCalledWith(
|
||||
expect(mockFetchEventSource).toHaveBeenCalledWith(
|
||||
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
|
||||
{ withCredentials: true, headers: {} },
|
||||
);
|
||||
});
|
||||
|
||||
it('should create eventsource with headers', async () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
{
|
||||
fetch: fetchApi.fetch,
|
||||
onerror: expect.any(Function),
|
||||
onmessage: expect.any(Function),
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'token' });
|
||||
await storageApi.syncEntityDocs(mockEntity);
|
||||
|
||||
expect(MockedEventSource).toHaveBeenCalledWith(
|
||||
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
|
||||
{ withCredentials: true, headers: { Authorization: 'Bearer token' } },
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve to cached', async () => {
|
||||
const myFetchApi = new MockFetchApi({
|
||||
injectIdentityAuth: { identityApi },
|
||||
});
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
fetchApi: myFetchApi,
|
||||
});
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual(
|
||||
@@ -160,17 +136,15 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": true}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' });
|
||||
});
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual(
|
||||
@@ -182,21 +156,18 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'log' && typeof fn === 'function') {
|
||||
fn({ data: '"A log message"' } as any);
|
||||
}
|
||||
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'log', data: '"A log message"' });
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
|
||||
});
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
const logHandler = jest.fn();
|
||||
@@ -212,25 +183,18 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onerror } = options;
|
||||
onerror?.(new NotFoundError('Some not found warning'));
|
||||
});
|
||||
|
||||
// we await later after we emitted the error
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
const promise = storageApi.syncEntityDocs(mockEntity).then();
|
||||
|
||||
// flush the event loop
|
||||
await new Promise(r => setTimeout(r));
|
||||
|
||||
const instance = MockedEventSource.mock
|
||||
.instances[0] as jest.Mocked<EventSource>;
|
||||
|
||||
instance.onerror?.({
|
||||
status: 404,
|
||||
message: 'Some not found warning',
|
||||
} as any);
|
||||
|
||||
await expect(promise).rejects.toThrow(NotFoundError);
|
||||
await expect(promise).rejects.toThrow('Some not found warning');
|
||||
});
|
||||
@@ -239,7 +203,6 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
@@ -247,16 +210,10 @@ describe('TechDocsStorageClient', () => {
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
const promise = storageApi.syncEntityDocs(mockEntity).then();
|
||||
|
||||
// flush the event loop
|
||||
await new Promise(r => setTimeout(r));
|
||||
|
||||
const instance = MockedEventSource.mock
|
||||
.instances[0] as jest.Mocked<EventSource>;
|
||||
|
||||
instance.onerror?.({
|
||||
type: 'error',
|
||||
data: 'Some other error',
|
||||
} as any);
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onerror } = options;
|
||||
onerror?.(new Error('Some other error'));
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow(Error);
|
||||
await expect(promise).rejects.toThrow('Some other error');
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
TechDocsMetadata,
|
||||
TechDocsStorageApi,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
|
||||
/**
|
||||
* API to talk to `techdocs-backend`.
|
||||
@@ -124,18 +124,17 @@ export class TechDocsClient implements TechDocsApi {
|
||||
export class TechDocsStorageClient implements TechDocsStorageApi {
|
||||
public configApi: Config;
|
||||
public discoveryApi: DiscoveryApi;
|
||||
public identityApi: IdentityApi;
|
||||
private fetchApi: FetchApi;
|
||||
|
||||
constructor(options: {
|
||||
configApi: Config;
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
fetchApi: FetchApi;
|
||||
/** @deprecated identityApi is not needed any more */
|
||||
identityApi?: IdentityApi;
|
||||
}) {
|
||||
this.configApi = options.configApi;
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.fetchApi = options.fetchApi;
|
||||
}
|
||||
|
||||
@@ -213,47 +212,32 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
|
||||
|
||||
const apiOrigin = await this.getApiOrigin();
|
||||
const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`;
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Polyfill is used to add support for custom headers and auth
|
||||
const source = new EventSourcePolyfill(url, {
|
||||
withCredentials: true,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
source.addEventListener('log', (e: any) => {
|
||||
if (e.data) {
|
||||
logHandler(JSON.parse(e.data));
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener('finish', (e: any) => {
|
||||
let updated: boolean = false;
|
||||
|
||||
if (e.data) {
|
||||
({ updated } = JSON.parse(e.data));
|
||||
}
|
||||
|
||||
resolve(updated ? 'updated' : 'cached');
|
||||
});
|
||||
|
||||
source.onerror = (e: any) => {
|
||||
source.close();
|
||||
|
||||
switch (e.status) {
|
||||
// the endpoint returned a 404 status
|
||||
case 404:
|
||||
reject(new NotFoundError(e.message));
|
||||
return;
|
||||
|
||||
// also handles the event-stream close. the reject is ignored if the Promise was already
|
||||
// resolved by a finish event.
|
||||
default:
|
||||
const ctrl = new AbortController();
|
||||
fetchEventSource(url, {
|
||||
fetch: this.fetchApi.fetch,
|
||||
signal: ctrl.signal,
|
||||
onmessage(e: any) {
|
||||
if (e.event === 'log') {
|
||||
if (e.data) {
|
||||
logHandler(JSON.parse(e.data));
|
||||
}
|
||||
} else if (e.event === 'finish') {
|
||||
let updated: boolean = false;
|
||||
if (e.data) {
|
||||
({ updated } = JSON.parse(e.data));
|
||||
}
|
||||
resolve(updated ? 'updated' : 'cached');
|
||||
} else if (e.event === 'error') {
|
||||
reject(new Error(e.data));
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
ctrl.abort();
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare module 'event-source-polyfill' {
|
||||
export class EventSourcePolyfill extends EventSource {
|
||||
constructor(
|
||||
url: string,
|
||||
options?: {
|
||||
withCredentials?: boolean;
|
||||
headers?: HeadersInit;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
createRoutableExtension,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
createSearchResultListItemExtension,
|
||||
@@ -52,14 +51,12 @@ export const techdocsPlugin = createPlugin({
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
|
||||
factory: ({ configApi, discoveryApi, fetchApi }) =>
|
||||
new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -36,11 +36,11 @@ const depTypes = [
|
||||
const roleRules = [
|
||||
{
|
||||
sourceRole: ['frontend-plugin', 'web-library'],
|
||||
targetRole: ['backend-plugin', 'node-library'],
|
||||
targetRole: ['backend-plugin', 'node-library', 'backend-plugin-module'],
|
||||
message: `Package SOURCE_NAME with frontend role SOURCE_ROLE has a dependency on package TARGET_NAME with backend role TARGET_ROLE, which is not permitted`,
|
||||
},
|
||||
{
|
||||
sourceRole: ['backend-plugin', 'node-library'],
|
||||
sourceRole: ['backend-plugin', 'node-library', 'backend-plugin-module'],
|
||||
targetRole: ['frontend-plugin', 'web-library'],
|
||||
message: `Package SOURCE_NAME with backend role SOURCE_ROLE has a dependency on package TARGET_NAME with frontend role TARGET_ROLE, which is not permitted`,
|
||||
},
|
||||
@@ -51,8 +51,9 @@ const roleRules = [
|
||||
'web-library',
|
||||
'backend-plugin',
|
||||
'node-library',
|
||||
'backend-plugin-module',
|
||||
],
|
||||
message: `Polymorphic package SOURCE_NAME has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`,
|
||||
message: `Polymorphic package SOURCE_NAME with role SOURCE_ROLE has a dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted since it's not also polymorphic`,
|
||||
},
|
||||
{
|
||||
sourceRole: ['frontend-plugin', 'web-library'],
|
||||
@@ -62,7 +63,7 @@ const roleRules = [
|
||||
'@backstage/plugin-api-docs',
|
||||
'@backstage/plugin-techdocs-addons-test-utils',
|
||||
],
|
||||
message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME, which is not permitted`,
|
||||
message: `Package SOURCE_NAME with role SOURCE_ROLE has a dependency on another plugin package TARGET_NAME with role TARGET_ROLE, which is not permitted`,
|
||||
},
|
||||
{
|
||||
sourceRole: ['frontend-plugin', 'web-library'],
|
||||
@@ -80,7 +81,7 @@ const roleRules = [
|
||||
'@backstage/plugin-techdocs-addons-test-utils',
|
||||
'@backstage/plugin-user-settings',
|
||||
],
|
||||
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
|
||||
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
|
||||
},
|
||||
{
|
||||
sourceRole: ['backend-plugin', 'node-library'],
|
||||
@@ -92,7 +93,7 @@ const roleRules = [
|
||||
'@backstage/backend-test-utils',
|
||||
'@backstage/backend-dynamic-feature-service',
|
||||
],
|
||||
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
|
||||
message: `Plugin package SOURCE_NAME with role SOURCE_ROLE has a runtime dependency on package TARGET_NAME with role TARGET_ROLE, which is not permitted. If you are using this dependency for dev server purposes, you can move it to devDependencies instead.`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -6979,6 +6979,7 @@ __metadata:
|
||||
"@material-ui/core": ^4.12.2
|
||||
"@material-ui/icons": ^4.9.1
|
||||
"@material-ui/lab": 4.0.0-alpha.61
|
||||
"@microsoft/fetch-event-source": ^2.0.1
|
||||
"@react-hookz/web": ^24.0.0
|
||||
"@rjsf/core": 5.18.2
|
||||
"@rjsf/material-ui": 5.18.2
|
||||
@@ -6993,7 +6994,6 @@ __metadata:
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
"@uiw/react-codemirror": ^4.9.3
|
||||
classnames: ^2.2.6
|
||||
event-source-polyfill: ^1.0.31
|
||||
git-url-parse: ^14.0.0
|
||||
humanize-duration: ^3.25.1
|
||||
json-schema: ^0.4.0
|
||||
@@ -7557,16 +7557,15 @@ __metadata:
|
||||
"@material-ui/icons": ^4.9.1
|
||||
"@material-ui/lab": 4.0.0-alpha.61
|
||||
"@material-ui/styles": ^4.10.0
|
||||
"@microsoft/fetch-event-source": ^2.0.1
|
||||
"@testing-library/dom": ^10.0.0
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^15.0.0
|
||||
"@testing-library/user-event": ^14.0.0
|
||||
"@types/dompurify": ^3.0.0
|
||||
"@types/event-source-polyfill": ^1.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
canvas: ^2.10.2
|
||||
dompurify: ^3.0.0
|
||||
event-source-polyfill: 1.0.25
|
||||
git-url-parse: ^14.0.0
|
||||
jss: ~10.10.0
|
||||
lodash: ^4.17.21
|
||||
@@ -10529,6 +10528,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@microsoft/fetch-event-source@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "@microsoft/fetch-event-source@npm:2.0.1"
|
||||
checksum: a50e1c0f33220206967266d0a4bbba0703e2793b079d9f6e6bfd48f71b2115964a803e14cf6e902c6fab321edc084f26022334f5eaacc2cec87f174715d41852
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@microsoft/microsoft-graph-types@npm:^2.6.0":
|
||||
version: 2.40.0
|
||||
resolution: "@microsoft/microsoft-graph-types@npm:2.40.0"
|
||||
@@ -14253,8 +14259,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@rollup/plugin-commonjs@npm:^25.0.0":
|
||||
version: 25.0.7
|
||||
resolution: "@rollup/plugin-commonjs@npm:25.0.7"
|
||||
version: 25.0.8
|
||||
resolution: "@rollup/plugin-commonjs@npm:25.0.8"
|
||||
dependencies:
|
||||
"@rollup/pluginutils": ^5.0.1
|
||||
commondir: ^1.0.1
|
||||
@@ -14267,7 +14273,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
checksum: 052e11839a9edc556eda5dcc759ab816dcc57e9f0f905a1e6e14fff954eaa6b1e2d0d544f5bd18d863993c5eba43d8ac9c19d9bb53b1c3b1213f32cfc9d50b2e
|
||||
checksum: dd105ee5625fbcaf832c0cf80be0aaf6a86bbd8fe99ff911f9ac4b78c79f26e9e99442b5aa0cc1136b5ddf89ec0b6c5728e5341ac04d687aef1b53063670b395
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17149,13 +17155,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/event-source-polyfill@npm:^1.0.0":
|
||||
version: 1.0.5
|
||||
resolution: "@types/event-source-polyfill@npm:1.0.5"
|
||||
checksum: f506b68710162f2ade1bccbc5691b8c67e5a703e565df2bc0b7b5be2637ba838ef81ec6c10b03248fe4d054386d95a6e827c7aace6e924986c2b9985f77b55de
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/expect@npm:^1.20.4":
|
||||
version: 1.20.4
|
||||
resolution: "@types/expect@npm:1.20.4"
|
||||
@@ -25522,20 +25521,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-source-polyfill@npm:1.0.25":
|
||||
version: 1.0.25
|
||||
resolution: "event-source-polyfill@npm:1.0.25"
|
||||
checksum: ed30428cc80eadfd693d267ba4a72dceaae938174cd116081ce38ad62bfd95f199430be7e8341e6f8f1e29489bbd5cfd4b3f6c8d6d463435623f7f91ae5f71b1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-source-polyfill@npm:^1.0.31":
|
||||
version: 1.0.31
|
||||
resolution: "event-source-polyfill@npm:1.0.31"
|
||||
checksum: 973f226404e2a1b14ed7ef15c718b89e213b41d7cfeeb1c10937fd09229f13904f3d7c3075ab28ccf858c213007559908eecdd577577330352f53a351383dd75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-target-shim@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "event-target-shim@npm:5.0.1"
|
||||
|
||||
Reference in New Issue
Block a user