diff --git a/.changeset/afraid-chairs-warn.md b/.changeset/afraid-chairs-warn.md deleted file mode 100644 index 1ae2a7513e..0000000000 --- a/.changeset/afraid-chairs-warn.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/errors': patch ---- - -Deprecate `parseErrorResponse` in favour of `parseErrorResponseBody`. Deprecate `data` field inside `ErrorResponse` in favour of `body`. -Rename the error name for unknown errors from `unknown` to `error`. diff --git a/.changeset/chilly-queens-bow.md b/.changeset/chilly-queens-bow.md new file mode 100644 index 0000000000..00e8628776 --- /dev/null +++ b/.changeset/chilly-queens-bow.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-azure-devops-common': patch +--- + +feat: Created pull request card component and initial pull request dashboard page. diff --git a/.changeset/clever-singers-search.md b/.changeset/clever-singers-search.md new file mode 100644 index 0000000000..0750cea8a8 --- /dev/null +++ b/.changeset/clever-singers-search.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Narrow the types returned by the request option functions, to only the specifics that they actually do return. The reason for this change is that a full `RequestInit` is unfortunate to return because it's different between `cross-fetch` and `node-fetch`. diff --git a/.changeset/curly-points-hide.md b/.changeset/curly-points-hide.md deleted file mode 100644 index ba513e5915..0000000000 --- a/.changeset/curly-points-hide.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-tech-insights-backend': patch -'@backstage/plugin-tech-insights-backend-module-jsonfc': patch ---- - -Update README docs to use correct function/parameter names diff --git a/.changeset/dry-pianos-brush.md b/.changeset/dry-pianos-brush.md new file mode 100644 index 0000000000..c749962a83 --- /dev/null +++ b/.changeset/dry-pianos-brush.md @@ -0,0 +1,51 @@ +--- +'@backstage/create-app': patch +--- + +Incorporate usage of the tokenManager into the backend created using `create-app`. + +In existing backends, update the `PluginEnvironment` to include a `tokenManager`: + +```diff +// packages/backend/src/types.ts + +... +import { + ... ++ TokenManager, +} from '@backstage/backend-common'; + +export type PluginEnvironment = { + ... ++ tokenManager: TokenManager; +}; +``` + +Then, create a `ServerTokenManager`. This can either be a `noop` that requires no secret and validates all requests by default, or one that uses a secret from your `app-config.yaml` to generate and validate tokens. + +```diff +// packages/backend/src/index.ts + +... +import { + ... ++ ServerTokenManager, +} from '@backstage/backend-common'; +... + +function makeCreateEnv(config: Config) { + ... + // CHOOSE ONE + // TokenManager not requiring a secret ++ const tokenManager = ServerTokenManager.noop(); + // OR TokenManager requiring a secret ++ const tokenManager = ServerTokenManager.fromConfig(config); + + ... + return (plugin: string): PluginEnvironment => { + ... +- return { logger, cache, database, config, reader, discovery }; ++ return { logger, cache, database, config, reader, discovery, tokenManager }; + }; +} +``` diff --git a/.changeset/early-bees-think.md b/.changeset/early-bees-think.md new file mode 100644 index 0000000000..ea77fa04c7 --- /dev/null +++ b/.changeset/early-bees-think.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Added apiVersionOverrides config to allow for specifying api versions to use for kubernetes objects diff --git a/.changeset/early-dragons-wave.md b/.changeset/early-dragons-wave.md deleted file mode 100644 index 641f36a9f3..0000000000 --- a/.changeset/early-dragons-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Bump `react-jsonschema-form` diff --git a/.changeset/forty-teachers-argue.md b/.changeset/forty-teachers-argue.md deleted file mode 100644 index 2e32719037..0000000000 --- a/.changeset/forty-teachers-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Switched to using the standardized JSON error responses for all provider endpoints. diff --git a/.changeset/fresh-pumas-collect.md b/.changeset/fresh-pumas-collect.md new file mode 100644 index 0000000000..15d36472ed --- /dev/null +++ b/.changeset/fresh-pumas-collect.md @@ -0,0 +1,19 @@ +--- +'@backstage/config-loader': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-badges-backend': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-todo-backend': patch +--- + +Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them diff --git a/.changeset/giant-bees-applaud.md b/.changeset/giant-bees-applaud.md deleted file mode 100644 index c4591dbc64..0000000000 --- a/.changeset/giant-bees-applaud.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Switch the default test coverage provider from the jest default one to `'v8'`, which provides much better coverage information when using the default Backstage test setup. This is considered a bug fix as the current coverage information is often very inaccurate. diff --git a/.changeset/green-toes-search.md b/.changeset/green-toes-search.md new file mode 100644 index 0000000000..2bdba7e31a --- /dev/null +++ b/.changeset/green-toes-search.md @@ -0,0 +1,28 @@ +--- +'@backstage/plugin-techdocs-backend': minor +--- + +**BREAKING** `DefaultTechDocsCollator` has a new required option `tokenManager`. See the create-app changelog for how to create a `tokenManager` and add it to the `PluginEnvironment`. It can then be passed to the collator in `createPlugin`: + +```diff +// packages/backend/src/plugins/search.ts + +... +export default async function createPlugin({ + ... ++ tokenManager, +}: PluginEnvironment) { + ... + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: DefaultTechDocsCollator.fromConfig(config, { + discovery, + logger, ++ tokenManager, + }), + }); + + ... +} +``` diff --git a/.changeset/hungry-impalas-wave.md b/.changeset/hungry-impalas-wave.md new file mode 100644 index 0000000000..376b9fc341 --- /dev/null +++ b/.changeset/hungry-impalas-wave.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-permission-node': minor +'@backstage/plugin-permission-backend': patch +--- + +Rename and adjust permission policy return type to reduce nesting diff --git a/.changeset/large-mugs-repair.md b/.changeset/large-mugs-repair.md deleted file mode 100644 index 5043fb80e1..0000000000 --- a/.changeset/large-mugs-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Disable ES transforms in tests transformed by the `jestSucraseTransform.js`. This is not considered a breaking change since all code is already transpiled this way in the development setup. diff --git a/.changeset/large-pears-agree.md b/.changeset/large-pears-agree.md deleted file mode 100644 index 60f755eff0..0000000000 --- a/.changeset/large-pears-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -DefaultTechDocsCollator is now included in the search backend, and the Search Page updated with the SearchType component that includes the techdocs type diff --git a/.changeset/lemon-moons-stare.md b/.changeset/lemon-moons-stare.md new file mode 100644 index 0000000000..4b7818d403 --- /dev/null +++ b/.changeset/lemon-moons-stare.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Create a `TokenManager` interface and `ServerTokenManager` implementation to generate and validate server tokens for authenticated backend-to-backend API requests. diff --git a/.changeset/light-cooks-train.md b/.changeset/light-cooks-train.md new file mode 100644 index 0000000000..435305998c --- /dev/null +++ b/.changeset/light-cooks-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Query CronJobs from Kubernetes with apiGroup BatchV1beta1 diff --git a/.changeset/long-spiders-bow.md b/.changeset/long-spiders-bow.md new file mode 100644 index 0000000000..c54641efe3 --- /dev/null +++ b/.changeset/long-spiders-bow.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +Remove the `backend:build-image` command from the CLI and added more deprecation warnings to other deprecated fields like `--lax` and `remove-plugin` diff --git a/.changeset/olive-rats-destroy.md b/.changeset/olive-rats-destroy.md new file mode 100644 index 0000000000..5908fc0f10 --- /dev/null +++ b/.changeset/olive-rats-destroy.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli': patch +'@techdocs/cli': patch +--- + +Bump react-dev-utils to v12 diff --git a/.changeset/pretty-trains-appear.md b/.changeset/pretty-trains-appear.md deleted file mode 100644 index 9a7b313524..0000000000 --- a/.changeset/pretty-trains-appear.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/cli': patch -'@backstage/core-app-api': patch -'@backstage/create-app': patch -'@backstage/techdocs-common': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-azure-devops-backend': patch -'@backstage/plugin-badges-backend': patch -'@backstage/plugin-bazaar-backend': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-jenkins-backend': patch -'@backstage/plugin-proxy-backend': patch -'@backstage/plugin-rollbar-backend': patch -'@backstage/plugin-search-backend': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Change default port of backend from 7000 to 7007. - -This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. - -You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: - -``` -backend: - listen: 0.0.0.0:7123 - baseUrl: http://localhost:7123 -``` - -More information can be found here: https://backstage.io/docs/conf/writing diff --git a/.changeset/purple-grapes-attack.md b/.changeset/purple-grapes-attack.md deleted file mode 100644 index a2a99690e9..0000000000 --- a/.changeset/purple-grapes-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-common': minor ---- - -Accept configApi rather than enabled flag in PermissionClient constructor. diff --git a/.changeset/rich-teachers-hide.md b/.changeset/rich-teachers-hide.md new file mode 100644 index 0000000000..27f437cbed --- /dev/null +++ b/.changeset/rich-teachers-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +build(dependencies): bump `style-loader` from 1.2.1 to 3.3.1 diff --git a/.changeset/rude-brooms-raise.md b/.changeset/rude-brooms-raise.md new file mode 100644 index 0000000000..6b5bd3ca3a --- /dev/null +++ b/.changeset/rude-brooms-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Tweaked the logged deprecation warning for `createRouteRef` to hopefully make it more clear. diff --git a/.changeset/search-zebras-matter.md b/.changeset/search-zebras-matter.md new file mode 100644 index 0000000000..277e36dd39 --- /dev/null +++ b/.changeset/search-zebras-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Add a new optional clearButton property to the SearchBar component. The default value for this new property is true. diff --git a/.changeset/sharp-carrots-press.md b/.changeset/sharp-carrots-press.md deleted file mode 100644 index a7c31d6759..0000000000 --- a/.changeset/sharp-carrots-press.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-config-schema': patch -'@backstage/plugin-scaffolder': patch ---- - -Fixed a missing `await` when throwing server side errors diff --git a/.changeset/smart-fans-complain.md b/.changeset/smart-fans-complain.md deleted file mode 100644 index db404f110d..0000000000 --- a/.changeset/smart-fans-complain.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -**BREAKING** EntitiesSearchFilter fields have changed. - -EntitiesSearchFilter now has only two fields: `key` and `value`. The `matchValueIn` and `matchValueExists` fields are no longer are supported. Previous filters written using the `matchValueIn` and `matchValueExists` fields can be rewritten as follows: - -Filtering by existence of key only: - -```diff - filter: { - { - key: 'abc', -- matchValueExists: true, - }, - } -``` - -Filtering by key and values: - -```diff - filter: { - { - key: 'abc', -- matchValueExists: true, -- matchValueIn: ['xyz'], -+ values: ['xyz'], - }, - } -``` - -Negation of filters can now be achieved through a `not` object: - -``` -filter: { - not: { - key: 'abc', - values: ['xyz'], - }, -} -``` diff --git a/.changeset/smooth-vans-boil.md b/.changeset/smooth-vans-boil.md deleted file mode 100644 index 39b2ea386f..0000000000 --- a/.changeset/smooth-vans-boil.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Index User entities by displayName to be able to search by full name. Added displayName (if present) to the 'text' field in the indexed document. diff --git a/.changeset/tasty-deers-play.md b/.changeset/tasty-deers-play.md new file mode 100644 index 0000000000..cf5322b0bd --- /dev/null +++ b/.changeset/tasty-deers-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Expose catalog lib in plugin-auth-backend, i.e `CatalogIdentityClient` class is exposed now. diff --git a/.changeset/thick-poems-camp.md b/.changeset/thick-poems-camp.md new file mode 100644 index 0000000000..72056dfb5a --- /dev/null +++ b/.changeset/thick-poems-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Fixed a bug where `useRouteRef` would fail in situations where relative navigation was needed and the app was is mounted on a sub-path. This would typically show up as a failure to navigate to a tab on an entity page. diff --git a/.changeset/twenty-worms-provide.md b/.changeset/twenty-worms-provide.md new file mode 100644 index 0000000000..ae231a1fa0 --- /dev/null +++ b/.changeset/twenty-worms-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add options to spawn in runCommand helper diff --git a/.changeset/weak-berries-sing.md b/.changeset/weak-berries-sing.md deleted file mode 100644 index 1b0cb8d719..0000000000 --- a/.changeset/weak-berries-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Update the default routes to use id instead of title diff --git a/.changeset/weak-rivers-perform.md b/.changeset/weak-rivers-perform.md new file mode 100644 index 0000000000..92f96ab6e0 --- /dev/null +++ b/.changeset/weak-rivers-perform.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Added accordions to display information on Jobs and CronJobs in the kubernetes plugin. Updated the PodsTable with fewer default columns and the ability to pass in additional ones depending on the use case. diff --git a/.changeset/wet-seas-deliver.md b/.changeset/wet-seas-deliver.md deleted file mode 100644 index da5679eb8e..0000000000 --- a/.changeset/wet-seas-deliver.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -'@backstage/core-app-api': patch -'@backstage/test-utils': patch ---- - -The `ApiRegistry` from `@backstage/core-app-api` class has been deprecated and will be removed in a future release. To replace it, we have introduced two new helpers that are exported from `@backstage/test-utils`, namely `TestApiProvider` and `TestApiRegistry`. - -These two new helpers are more tailored for writing tests and development setups, as they allow for partial implementations of each of the APIs. - -When migrating existing code it is typically best to prefer usage of `TestApiProvider` when possible, so for example the following code: - -```tsx -render( - - {...} - -) -``` - -Would be migrated to this: - -```tsx -render( - - {...} - -) -``` - -In cases where the `ApiProvider` is used in a more standalone way, for example to reuse a set of APIs across multiple tests, the `TestApiRegistry` can be used instead. Note that the `TestApiRegistry` only has a single static factory method, `.from()`, and it is slightly different from the existing `.from()` method on `ApiRegistry` in that it doesn't require the API pairs to be wrapped in an outer array. - -Usage that looks like this: - -```ts -const apis = ApiRegistry.with( - identityApiRef, - mockIdentityApi as unknown as IdentityApi, -).with(configApiRef, new ConfigReader({})); -``` - -OR like this: - -```ts -const apis = ApiRegistry.from([ - [identityApiRef, mockIdentityApi as unknown as IdentityApi], - [configApiRef, new ConfigReader({})], -]); -``` - -Would be migrated to this: - -```ts -const apis = TestApiRegistry.from( - [identityApiRef, mockIdentityApi], - [configApiRef, new ConfigReader({})], -); -``` - -If your app is still using the `ApiRegistry` to construct the `apis` for `createApp`, we recommend that you move over to use the new method of supplying API factories instead, using `createApiFactory`. diff --git a/.changeset/witty-cats-tell.md b/.changeset/witty-cats-tell.md new file mode 100644 index 0000000000..82e33bc0a6 --- /dev/null +++ b/.changeset/witty-cats-tell.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add group filtering to the scaffolder page so that individuals can surface specific templates to end users ahead of others, or group templates together. This can be accomplished by passing in a `groups` prop to the `ScaffolderPage` + +``` + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} +/> +``` diff --git a/.changeset/yellow-pandas-draw.md b/.changeset/yellow-pandas-draw.md deleted file mode 100644 index 6d4f8116b3..0000000000 --- a/.changeset/yellow-pandas-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Pin sidebar by default for easier navigation diff --git a/.changeset/young-bikes-argue.md b/.changeset/young-bikes-argue.md new file mode 100644 index 0000000000..a9f09cd12b --- /dev/null +++ b/.changeset/young-bikes-argue.md @@ -0,0 +1,27 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**BREAKING** `DefaultCatalogCollator` has a new required option `tokenManager`. See the create-app changelog for how to create a `tokenManager` and add it to the `PluginEnvironment`. It can then be passed to the collator in `createPlugin`: + +```diff +// packages/backend/src/plugins/search.ts + +... +export default async function createPlugin({ + ... ++ tokenManager, +}: PluginEnvironment) { + ... + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: DefaultCatalogCollator.fromConfig(config, { + discovery, ++ tokenManager, + }), + }); + + ... +} +``` diff --git a/.changeset/young-sheep-impress.md b/.changeset/young-sheep-impress.md deleted file mode 100644 index b69447d4d4..0000000000 --- a/.changeset/young-sheep-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-graphiql': patch ---- - -Letting GraphiQL use headers diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9be92d294e..81691c74bb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ /docs/features/techdocs @backstage/techdocs-core /docs/features/search @backstage/techdocs-core /docs/assets/search @backstage/techdocs-core +/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps /plugins/code-coverage @backstage/reviewers @alde @nissayeva /plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva /plugins/cost-insights @backstage/silver-lining @@ -18,6 +19,15 @@ /plugins/techdocs-backend @backstage/techdocs-core /plugins/ilert @backstage/reviewers @yacut /plugins/home @backstage/techdocs-core +/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin +/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin +/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin +/plugins/kafka @backstage/reviewers @nirga +/plugins/kafka-backend @backstage/reviewers @nirga +/tech-insights-backend @backstage/reviewers @xantier @iain-b +/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b +/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b +/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b /packages/embedded-techdocs-app @backstage/techdocs-core /packages/search-common @backstage/techdocs-core /packages/techdocs-cli @backstage/techdocs-core diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 3cd0d53720..0000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - - plugin - - help wanted - - good first issue - - rfc -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index d65e31b4c4..4ec62923d8 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -189,6 +189,7 @@ oidc Okta onboarding Onboarding +OpenShift orgs pagerduty pageview diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..96a2a20109 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +name: 'Stale workflow' +on: + workflow_dispatch: + schedule: + - cron: '*/10 * * * *' # run every 10 minutes as it also removes labels. + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@main + id: stale + with: + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + days-before-issue-stale: 60 + days-before-issue-close: 7 + exempt-issue-labels: 'pinned,security,plugin,help wanted,good first issue,rfc' + stale-issue-label: stale + stale-pr-message: > + This PR has been automatically marked as stale because it has not had + recent activity from the author. It will be closed if no further activity occurs. + If you are the author and the PR has been closed, feel free to re-open the PR and continue the contribution! + days-before-pr-stale: 7 + days-before-pr-close: 3 + exempt-pr-labels: reviewer-approved,awaiting-review + stale-pr-label: stale + operations-per-run: 100 diff --git a/ADOPTERS.md b/ADOPTERS.md index 67aa8ec709..529c22d144 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -66,8 +66,10 @@ | [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | | [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | | [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | -| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | -| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | +| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | +| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | | [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | -| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | -| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | \ No newline at end of file +| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | +| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | +| [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | +| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | diff --git a/app-config.yaml b/app-config.yaml index 50ddb7ce89..913a72dbab 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -23,6 +23,11 @@ app: title: '#backstage' backend: + # Used for enabling authentication, secret is shared by all backend plugins + # See backend-to-backend-auth.md in the docs for information on the format + # auth: + # keys: + # - secret: ${BACKEND_SECRET} baseUrl: http://localhost:7007 listen: port: 7007 diff --git a/contrib/chart/backstage/templates/ingress.yaml b/contrib/chart/backstage/templates/ingress.yaml index 7231afda13..9340467800 100644 --- a/contrib/chart/backstage/templates/ingress.yaml +++ b/contrib/chart/backstage/templates/ingress.yaml @@ -1,7 +1,13 @@ {{- $frontendUrl := urlParse .Values.appConfig.app.baseUrl}} {{- $backendUrl := urlParse .Values.appConfig.backend.baseUrl}} {{- $lighthouseUrl := urlParse .Values.appConfig.lighthouse.baseUrl}} + +{{/* Determine the api type for the ingress */}} +{{- if lt .Capabilities.KubeVersion.Minor "19" }} apiVersion: networking.k8s.io/v1beta1 +{{- else if ge .Capabilities.KubeVersion.Minor "19" }} +apiVersion: networking.k8s.io/v1 +{{- end }} kind: Ingress metadata: name: {{ include "backstage.fullname" . }}-ingress diff --git a/docs/assets/software-templates/grouped-templates.png b/docs/assets/software-templates/grouped-templates.png new file mode 100644 index 0000000000..9a3689ad1e Binary files /dev/null and b/docs/assets/software-templates/grouped-templates.png differ diff --git a/docs/auth/index.md b/docs/auth/index.md index 0c03e33900..4a8d225048 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -16,8 +16,10 @@ Backstage identity information in your app or plugins. Backstage comes with many common authentication providers in the core library: +- [Atlassian](atlassian/provider.md) - [Auth0](auth0/provider.md) - [Azure](microsoft/provider.md) +- [Bitbucket](bitbucket/provider.md) - [GitHub](github/provider.md) - [GitLab](gitlab/provider.md) - [Google](google/provider.md) diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 7cbd3cea89..6f58abe769 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -7,9 +7,9 @@ description: How to build a Backstage Docker image for deployment This section describes how to build a Backstage App into a deployable Docker image. It is split into three sections, first covering the host build approach, -which is recommended due its speed and more efficient and often simpler caching. -The second section covers a full multi-stage Docker build, and the last section -covers how to deploy the frontend and backend as separate images. +which is recommended due to its speed and more efficient and often simpler +caching. The second section covers a full multi-stage Docker build, and the last +section covers how to deploy the frontend and backend as separate images. Something that goes for all of these docker deployment strategies is that they are stateless, so for a production deployment you will want to set up and diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 6b3991144b..a7f9b0cd1e 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -219,6 +219,26 @@ The custom resource's apiVersion. The plural representing the custom resource. +### `apiVersionOverrides` (optional) + +Overrides for the API versions used to make requests for the corresponding +objects. If using a legacy Kubernetes version, you may use this config to +override the default API versions to ones that are supported by your cluster. + +Example: + +```yaml +--- +kubernetes: + apiVersionOverrides: + cronjobs: 'v1beta1' +``` + +For more information on which API versions are supported by your cluster, please +view the Kubernetes API docs for your Kubernetes version (e.g. +[API Groups for v1.22](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#-strong-api-groups-strong-) +) + ### Role Based Access Control The current RBAC permissions required are read-only cluster wide, for the diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 27219e529a..7c3717e22e 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -154,13 +154,17 @@ import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; export default async function createPlugin({ logger, discovery, + tokenManager, }: PluginEnvironment) { const searchEngine = new LunrSearchEngine({ logger }); const indexBuilder = new IndexBuilder({ logger, searchEngine }); indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: new DefaultCatalogCollator({ + discovery, + tokenManager, + }), }); const { scheduler } = await indexBuilder.build(); @@ -285,7 +289,10 @@ const indexBuilder = new IndexBuilder({ logger, searchEngine }); indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: new DefaultCatalogCollator({ + discovery, + tokenManager, + }), }); indexBuilder.addCollator({ @@ -303,6 +310,9 @@ its `defaultRefreshIntervalSeconds` value, like this: ```typescript {3} indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: new DefaultCatalogCollator({ + discovery, + tokenManager, + }), }); ``` diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 621d46b51e..4ba44b8ef4 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -2,7 +2,7 @@ id: how-to-guides title: Search "HOW TO" guides sidebar_label: "HOW TO" guides -description: Search "HOW TO" guides +description: Search "HOW TO" guides --- ## How to implement your own Search API @@ -74,6 +74,7 @@ indexBuilder.addCollator({ collator: DefaultTechDocsCollator.fromConfig(config, { discovery, logger, + tokenManager, }), }); ``` diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index a0302ce279..1331c73b72 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -4,9 +4,9 @@ title: Search Engines description: Choosing and configuring your search engine for Backstage --- -Backstage supports 2 search engines by default, an in-memory engine called Lunr -and ElasticSearch. You can configure your own search engines by implementing the -provided interface as mentioned in the +Backstage supports 3 search engines by default, an in-memory engine called Lunr, +ElasticSearch and Postgres. You can configure your own search engines by +implementing the provided interface as mentioned in the [search backend documentation.](./getting-started.md#Backend) Provided search engine implementations have their own way of constructing diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index 737f77f67b..b57bb96818 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -53,3 +53,30 @@ You can do so by including the following lines in the last step of your RUN apt-get update && apt-get install -y python3 python3-pip RUN pip3 install cookiecutter ``` + +### Customizing the ScaffolderPage with Grouping and Filtering + +Once you have more than a few software templates you may want to customize your +`ScaffolderPage` by grouping and surfacing certain templates together. You can +accomplish this by creating `groups` and passing them to your `ScaffolderPage` +like below + +``` + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} +/> +``` + +This code will group all templates with the 'recommended' tag together at the +top of the page above any other templates not filtered by this group or others. + +You can also further customize groups by passing in a `titleComponent` instead +of a `title` which will be a component to use as the header instead of just the +default `ContentHeader` with the `title` set as it's value. +![Grouped Templates](../../assets/software-templates/grouped-templates.png) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 5a970ef143..02e2e4d51d 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -23,7 +23,7 @@ guide to do a repository-based installation. - Access to a Linux-based operating system, such as Linux, MacOS or [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/) -- An account with elevated rights +- An account with elevated rights to install the dependencies - `curl` or `wget` installed - Node.js Active LTS Release installed (currently v14) using one of these methods: @@ -36,15 +36,16 @@ guide to do a repository-based installation. - `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install) - `docker` [installation](https://docs.docker.com/engine/install/) - `git` [installation](https://github.com/git-guides/install-git) -- If the system is not directly accessible over your network, the following - ports need to be opened: 3000, 7007 +- If the system is not directly accessible over your network the following ports + need to be opened: 3000, 7007. This is quite uncommon, unless when you're + installing in a container, VM or remote system. ### Create your Backstage App To install the Backstage Standalone app, we make use of `npx`, a tool to run -Node executables straight from the registry. Running the command below will -install Backstage. The wizard will create a subdirectory inside your current -working directory. +Node executables straight from the registry. This tool is part of your Node.js +installation. Running the command below will install Backstage. The wizard will +create a subdirectory inside your current working directory. ```bash npx @backstage/create-app @@ -78,12 +79,21 @@ yarn dev It might take a little while, but as soon as the message `[0] webpack compiled successfully` appears, you can open a browser and directly navigate to your freshly installed Backstage portal at `http://localhost:3000`. -You can start exploring the demo immediately. +You can start exploring the demo immediately. Please note that the in-memory +database will be cleared when you restart the app, so you'll most likely want to +carry on with the database steps.

Screenshot of the Backstage portal.

+The most common next steps are to move to a persistent database, configure +authentication, and add a plugin: + +- [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres) +- [Setting up Authentication](https://backstage.io/docs/auth/) +- [Adding a plugin](https://backstage.io/docs/getting-started/configure-app-with-plugins) + Congratulations! That should be it. Let us know how it went: [on discord](https://discord.gg/EBHEGzX), file issues for any [feature](https://github.com/backstage/backstage/issues/new?labels=help+wanted&template=feature_template.md) @@ -93,10 +103,3 @@ or [bugs](https://github.com/backstage/backstage/issues/new?labels=bug&template=bug_template.md) you have, and feel free to [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)! - -The most common next steps are to configure Backstage, add a plugin and moving -to a more persistent database: - -- [Setting up Authentication](https://backstage.io/docs/auth/) -- [Switching from SQLite to PostgresQL](https://backstage.io/docs/tutorials/switching-sqlite-postgres) -- [Adding a plugin](https://backstage.io/docs/getting-started/configure-app-with-plugins) diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md new file mode 100644 index 0000000000..ad452f47af --- /dev/null +++ b/docs/integrations/azure/discovery.md @@ -0,0 +1,49 @@ +--- +id: discovery +title: Azure DevOps Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from repositories in an Azure DevOps organization +--- + +The Azure DevOps integration has a special discovery processor for discovering +catalog entities within an Azure DevOps. The processor will crawl the Azure +DevOps organization and register entities matching the configured path. This can +be useful as an alternative to static locations or manually adding things to the +catalog. + +To use the discovery processor, you'll need a GitHub integration +[set up](locations.md) with a `AZURE_TOKEN`. Then you can add a location target +to the catalog configuration: + +```yaml +catalog: + locations: + # Scan all repositories for a catalog-info.yaml in the root of the default branch + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject + # Or use a custom pattern for a subset of all repositories with default repository + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject/_git/service-* + # Or use a custom file format and location + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject/_git/*?path=/src/*/catalog-info.yaml +``` + +Note the `azure-discovery` type, as this is not a regular `url` processor. + +When using a custom pattern, the target is composed of five parts: + +- The base instance URL, `https://dev.azure.com` in this case +- The organization name which is required, `myorg` in this case +- The project name which is required, `myproject` in this case +- The repository blob to scan, which accepts \* wildcard tokens and must be + added after `_git/`. This can simply be `*` to scan all repositories in the + project. +- The path within each repository to find the catalog YAML file. This will + usually be `/catalog-info.yaml`, `/src/*/catalog-info.yaml` or a similar + variation for catalog files stored in the root directory of each repository. + +_Note:_ the path parameter follows the same rules as the search on Azure DevOps +web interface. For more details visit the +[official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops) diff --git a/docs/overview/stability-index.md b/docs/overview/stability-index.md index a79bcd6e11..be02ba8f07 100644 --- a/docs/overview/stability-index.md +++ b/docs/overview/stability-index.md @@ -27,7 +27,7 @@ point building on top of the previous one: and the new APIs can be used in parallel. This deprecation must have been released for at least two weeks before the deprecated API is removed in a minor version bump. -- **3** - The time limit for the deprecation is 3 months instead of two days. +- **3** - The time limit for the deprecation is 3 months instead of two weeks. TL;DR: diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 7adf2650df..ace9a89968 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -261,10 +261,13 @@ analytics events captured. Use it like this: ```tsx -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { analyticsApiRef } from '@backstage/core-plugin-api'; -import { MockAnalyticsApi, wrapInTestApp } from '@backstage/test-utils'; import { render, fireEvent, waitFor } from '@testing-library/react'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; +import { + MockAnalyticsApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; describe('SomeComponent', () => { it('should capture event on click', () => { @@ -274,9 +277,9 @@ describe('SomeComponent', () => { // Render the component being tested const { getByText } = render( wrapInTestApp( - + - , + , ), ); diff --git a/docs/tutorials/backend-to-backend-auth.md b/docs/tutorials/backend-to-backend-auth.md new file mode 100644 index 0000000000..3cb72a4827 --- /dev/null +++ b/docs/tutorials/backend-to-backend-auth.md @@ -0,0 +1,68 @@ +--- +id: backend-to-backend-auth +title: Backend-to-Backend Authentication +description: + Guide for authenticating API requests between Backstage plugin backends +--- + +This tutorial describes the steps needed to handle _backend-to-backend +authentication_, which allows plugin backends to determine whether a given +request originates from a legitimate Backstage backend by verifying a token +signed with a shared secret. This system has limited use for now, but will be +needed to support the upcoming framework for permissions and authorization (see +[the PRFC on the topic](https://github.com/backstage/backstage/pull/7761) for +more details). + +Backends have no concept of a Backstage identity, so instead they use a token +generated using a shared key stored in config. You can generate a unique key for +your app in a terminal, and set the `BACKEND_SECRET` environment variable to the +resulting value. + +```bash +node -p 'require("crypto").randomBytes(24).toString("base64")' +``` + +Requests originating from a backend plugin can be authenticated by decorating +them with a backend token. Backend tokens can be generated using a +`TokenManager`, which can be passed to plugin backends via the +`PluginEnvironment`. The `TokenManager` provided in new Backstage instances +generated by `create-app` is a stub, which returns empty tokens and accepts any +input string as valid. To enable backend-to-backend authentication, you'll need +to instantiate a new one using the secret from your config instead: + +```diff +// packages/backend/src/index.ts + +function makeCreateEnv(config: Config) { + const root = getRootLogger(); + const reader = UrlReaders.default({ logger: root, config }); + const discovery = SingleHostDiscovery.fromConfig(config); + + root.info(`Created UrlReader ${reader}`); + + const cacheManager = CacheManager.fromConfig(config); + const databaseManager = DatabaseManager.fromConfig(config); +- const tokenManager = ServerTokenManager.noop(); ++ const tokenManager = ServerTokenManager.fromConfig(config); +``` + +With this `tokenManager`, you can then generate a server token for requests: + +```typescript +const { token } = await this.tokenManager.getToken(); + +const response = await fetch(pluginBackendApiUrl, { + method: 'GET', + headers: { + ...headers, + Authorization: `Bearer ${token}`, + }, +}); +``` + +You can use the same `tokenManager` to authenticate tokens supplied on incoming +requests: + +```typescript +await tokenManager.authenticate(token); // throws if token is invalid +``` diff --git a/microsite/package.json b/microsite/package.json index a2a2fd98de..a24de8935d 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -19,7 +19,7 @@ "@spotify/prettier-config": "^12.0.0", "docusaurus": "^2.0.0-alpha.70", "js-yaml": "^4.1.0", - "prettier": "^2.4.1", + "prettier": "^2.5.0", "yarn-lock-check": "^1.0.5" }, "prettier": "@spotify/prettier-config" diff --git a/microsite/sidebars.json b/microsite/sidebars.json index a24d5897ab..89eb282d14 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -125,7 +125,11 @@ { "type": "subcategory", "label": "Azure", - "ids": ["integrations/azure/locations", "integrations/azure/org"] + "ids": [ + "integrations/azure/locations", + "integrations/azure/discovery", + "integrations/azure/org" + ] }, { "type": "subcategory", diff --git a/microsite/yarn.lock b/microsite/yarn.lock index c03ecf989a..9946f4914d 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -5190,10 +5190,10 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c" - integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA== +prettier@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893" + integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg== prismjs@^1.22.0: version "1.25.0" diff --git a/mkdocs.yml b/mkdocs.yml index 48df25afef..2eed377819 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,7 @@ nav: - Discovery: 'integrations/aws-s3/discovery.md' - Azure: - Locations: 'integrations/azure/locations.md' + - Discovery: 'integrations/azure/discovery.md' - Org Data: 'integrations/azure/org.md' - Bitbucket: - Locations: 'integrations/bitbucket/locations.md' diff --git a/package.json b/package.json index 16ba8461ac..9adbb1c821 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "prettier:check": "prettier --check .", "lerna": "lerna", "storybook": "yarn workspace storybook start", + "snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false", + "snyk:test:package": "yarn snyk:test --include", "build-storybook": "yarn workspace storybook build-storybook", "techdocs-cli": "node scripts/techdocs-cli.js", "techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js", diff --git a/packages/app/package.json b/packages/app/package.json index 5b9e97d317..09141f7cf7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -66,7 +66,7 @@ "devDependencies": { "@backstage/test-utils": "^0.1.22", "@rjsf/core": "^3.2.1", - "@testing-library/cypress": "^7.0.1", + "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 401665b62e..7e5d3db926 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -34,6 +34,7 @@ import { SignInPage, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; +import { AzurePullRequestsPage } from '@backstage/plugin-azure-devops'; import { CatalogEntityPage, CatalogIndexPage, @@ -175,7 +176,20 @@ const routes = ( > {techDocsPage} - }> + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} + /> + } + > @@ -203,6 +217,7 @@ const routes = ( element={} /> } /> + } /> ); diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index f365310595..e1496b556c 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -41,6 +41,7 @@ import { SidebarSpace, SidebarScrollWrapper, } from '@backstage/core-components'; +import { AzurePullRequestsIcon } from '@backstage/plugin-azure-devops'; const useSidebarLogoStyles = makeStyles({ root: { @@ -94,6 +95,11 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( + diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 089cb9c7cf..a6878cd28b 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { EntityLayout } from '@backstage/plugin-catalog'; import { DefaultStarredEntitiesApi, @@ -22,7 +21,11 @@ import { starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { githubActionsApiRef } from '@backstage/plugin-github-actions'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import React from 'react'; import { cicdContent } from './EntityPage'; @@ -45,22 +48,22 @@ describe('EntityPage Test', () => { const mockedApi = { listWorkflowRuns: jest.fn().mockResolvedValue([]), - getWorkflow: jest.fn(), - getWorkflowRun: jest.fn(), - reRunWorkflow: jest.fn(), - listJobsForWorkflowRun: jest.fn(), - downloadJobLogsForWorkflowRun: jest.fn(), - } as jest.Mocked; - - const apis = ApiRegistry.with(githubActionsApiRef, mockedApi).with( - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ); + }; describe('cicdContent', () => { it('Should render GitHub Actions View', async () => { const rendered = await renderInTestApp( - + @@ -68,7 +71,7 @@ describe('EntityPage Test', () => { - , + , ); expect(rendered.getByText('ExampleComponent')).toBeInTheDocument(); diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 3ab36cc679..869bf46019 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/backend-common +## 0.9.11 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + ## 0.9.10 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index e3119e78a8..9c66c0573e 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -526,6 +526,20 @@ export type SearchResponseFile = { content(): Promise; }; +// @public +export class ServerTokenManager implements TokenManager { + // (undocumented) + authenticate(token: string): Promise; + // (undocumented) + static fromConfig(config: Config): ServerTokenManager; + // (undocumented) + getToken(): Promise<{ + token: string; + }>; + // (undocumented) + static noop(): TokenManager; +} + // @public (undocumented) export type ServiceBuilder = { loadConfig(config: Config): ServiceBuilder; @@ -583,6 +597,16 @@ export interface StatusCheckHandlerOptions { statusCheck?: StatusCheck; } +// @public +export interface TokenManager { + // (undocumented) + authenticate: (token: string) => Promise; + // (undocumented) + getToken: () => Promise<{ + token: string; + }>; +} + // @public export type UrlReader = { read(url: string): Promise; diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 3690069431..bdc7740c13 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -20,6 +20,20 @@ export interface Config { }; backend: { + /** Backend configuration for when request authentication is enabled */ + auth?: { + /** Keys shared by all backends for signing and validating backend tokens. */ + keys: { + /** + * Secret for generating tokens. Should be a base64 string, recommended + * length is 24 bytes. + * + * @visibility secret + */ + secret: string; + }[]; + }; + baseUrl: string; // defined in core, but repeated here without doc /** Address that the backend should listen to. */ diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 9e0187137d..7fb2de0fe0 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.9.10", + "version": "0.9.11", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -32,7 +32,7 @@ "@backstage/cli-common": "^0.1.6", "@backstage/config": "^0.1.11", "@backstage/config-loader": "^0.8.0", - "@backstage/errors": "^0.1.4", + "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.9", "@backstage/types": "^0.1.1", "@google-cloud/storage": "^5.8.0", @@ -54,6 +54,7 @@ "git-url-parse": "^11.6.0", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", + "jose": "^1.27.1", "keyv": "^4.0.3", "keyv-memcache": "^1.2.5", "knex": "^0.95.1", @@ -80,8 +81,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/test-utils": "^0.1.23", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^1.6.0", diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index c214961a2e..e430ddc1a4 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -31,4 +31,5 @@ export * from './paths'; export * from './reading'; export * from './scm'; export * from './service'; +export * from './tokens'; export * from './util'; diff --git a/packages/backend-common/src/tokens/ServerTokenManager.test.ts b/packages/backend-common/src/tokens/ServerTokenManager.test.ts new file mode 100644 index 0000000000..fe724a0a4b --- /dev/null +++ b/packages/backend-common/src/tokens/ServerTokenManager.test.ts @@ -0,0 +1,189 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { TokenManager } from './types'; +import { ServerTokenManager } from './ServerTokenManager'; + +const emptyConfig = new ConfigReader({}); +const configWithSecret = new ConfigReader({ + backend: { auth: { keys: [{ secret: 'a-secret-key' }] } }, +}); + +describe('ServerTokenManager', () => { + it('should throw if secret in config does not exist', () => { + expect(() => ServerTokenManager.fromConfig(emptyConfig)).toThrowError(); + }); + + describe('getToken', () => { + it('should return a token if secret in config exists', async () => { + const tokenManager = ServerTokenManager.fromConfig(configWithSecret); + expect((await tokenManager.getToken()).token).toBeDefined(); + }); + + it('should return a token string if using a noop TokenManager', async () => { + const tokenManager = ServerTokenManager.noop(); + expect((await tokenManager.getToken()).token).toBeDefined(); + }); + }); + + describe('authenticate', () => { + it('should not throw if token is valid', async () => { + const tokenManager = ServerTokenManager.fromConfig(configWithSecret); + const { token } = await tokenManager.getToken(); + await expect(tokenManager.authenticate(token)).resolves.not.toThrow(); + }); + + it('should throw if token is invalid', async () => { + const tokenManager = ServerTokenManager.fromConfig(configWithSecret); + await expect( + tokenManager.authenticate('random-string'), + ).rejects.toThrowError(/invalid server token/i); + }); + + it('should validate server tokens created by a different instance using the same secret', async () => { + const tokenManager1 = ServerTokenManager.fromConfig(configWithSecret); + const tokenManager2 = ServerTokenManager.fromConfig(configWithSecret); + + const { token } = await tokenManager1.getToken(); + + await expect(tokenManager2.authenticate(token)).resolves.not.toThrow(); + }); + + it('should validate server tokens created using any of the secrets', async () => { + const tokenManager1 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'a1b2c3' }] } }, + }), + ); + const tokenManager2 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'd4e5f6' }] } }, + }), + ); + const tokenManager3 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { + auth: { keys: [{ secret: 'a1b2c3' }, { secret: 'd4e5f6' }] }, + }, + }), + ); + + const { token: token1 } = await tokenManager1.getToken(); + await expect(tokenManager3.authenticate(token1)).resolves.not.toThrow(); + + const { token: token2 } = await tokenManager2.getToken(); + await expect(tokenManager3.authenticate(token2)).resolves.not.toThrow(); + }); + + it('should throw for server tokens created using a different secret', async () => { + const tokenManager1 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'a1b2c3' }] } }, + }), + ); + const tokenManager2 = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'd4e5f6' }] } }, + }), + ); + + const { token } = await tokenManager1.getToken(); + + await expect(tokenManager2.authenticate(token)).rejects.toThrowError( + /invalid server token/i, + ); + }); + + it('should throw for server tokens created using a noop TokenManager', async () => { + const noopTokenManager = ServerTokenManager.noop(); + const tokenManager = ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [{ secret: 'a1b2c3' }] } }, + }), + ); + + const { token } = await noopTokenManager.getToken(); + + await expect(tokenManager.authenticate(token)).rejects.toThrowError( + /invalid server token/i, + ); + }); + }); + + describe('ServerTokenManager.fromConfig', () => { + it('should throw if backend auth configuration is missing', () => { + expect(() => + ServerTokenManager.fromConfig(new ConfigReader({})), + ).toThrow(); + }); + + it('should throw if no keys are included in the configuration', () => { + expect(() => + ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { auth: { keys: [] } }, + }), + ), + ).toThrow(); + }); + + it('should throw if any key is missing a secret property', () => { + expect(() => + ServerTokenManager.fromConfig( + new ConfigReader({ + backend: { + auth: { + keys: [{ secret: '123' }, {}, { secret: '789' }], + }, + }, + }), + ), + ).toThrow(); + }); + }); + + describe('ServerTokenManager.noop', () => { + let noopTokenManager: TokenManager; + + beforeEach(() => { + noopTokenManager = ServerTokenManager.noop(); + }); + + it('should accept tokens it generates', async () => { + const { token } = await noopTokenManager.getToken(); + + await expect(noopTokenManager.authenticate(token)).resolves.not.toThrow(); + }); + + it('should accept tokens generated by other noop token managers', async () => { + const noopTokenManager2 = ServerTokenManager.noop(); + await expect( + noopTokenManager.authenticate( + ( + await noopTokenManager2.getToken() + ).token, + ), + ).resolves.not.toThrow(); + }); + + it('should accept signed tokens', async () => { + const tokenManager = ServerTokenManager.fromConfig(configWithSecret); + await expect( + noopTokenManager.authenticate((await tokenManager.getToken()).token), + ).resolves.not.toThrow(); + }); + }); +}); diff --git a/packages/backend-common/src/tokens/ServerTokenManager.ts b/packages/backend-common/src/tokens/ServerTokenManager.ts new file mode 100644 index 0000000000..35a07509b6 --- /dev/null +++ b/packages/backend-common/src/tokens/ServerTokenManager.ts @@ -0,0 +1,80 @@ +/* + * 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 { JWKS, JWK, JWT } from 'jose'; +import { Config } from '@backstage/config'; +import { AuthenticationError } from '@backstage/errors'; +import { TokenManager } from './types'; + +class NoopTokenManager implements TokenManager { + async getToken() { + return { token: '' }; + } + + async authenticate() {} +} + +/** + * Creates and validates tokens for use during backend-to-backend + * authentication. + * + * @public + */ +export class ServerTokenManager implements TokenManager { + private readonly verificationKeys: JWKS.KeyStore; + private readonly signingKey: JWK.Key; + + static noop(): TokenManager { + return new NoopTokenManager(); + } + + static fromConfig(config: Config) { + return new ServerTokenManager( + config + .getConfigArray('backend.auth.keys') + .map(key => key.getString('secret')), + ); + } + + private constructor(secrets?: string[]) { + if (!secrets?.length) { + throw new Error( + 'No secrets provided when constructing ServerTokenManager', + ); + } + + this.verificationKeys = new JWKS.KeyStore( + secrets.map(k => JWK.asKey({ kty: 'oct', k })), + ); + this.signingKey = this.verificationKeys.all()[0]; + } + + async getToken(): Promise<{ token: string }> { + const jwt = JWT.sign({ sub: 'backstage-server' }, this.signingKey, { + algorithm: 'HS256', + }); + + return { token: jwt }; + } + + async authenticate(token: string): Promise { + try { + JWT.verify(token, this.verificationKeys); + } catch (e) { + throw new AuthenticationError('Invalid server token'); + } + } +} diff --git a/packages/backend-common/src/tokens/index.ts b/packages/backend-common/src/tokens/index.ts new file mode 100644 index 0000000000..43ff12e597 --- /dev/null +++ b/packages/backend-common/src/tokens/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { ServerTokenManager } from './ServerTokenManager'; +export type { TokenManager } from './types'; diff --git a/packages/backend-common/src/tokens/types.ts b/packages/backend-common/src/tokens/types.ts new file mode 100644 index 0000000000..1fea018db9 --- /dev/null +++ b/packages/backend-common/src/tokens/types.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * Interface for creating and validating tokens. + * + * @public + */ +export interface TokenManager { + getToken: () => Promise<{ token: string }>; + authenticate: (token: string) => Promise; +} diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 7cf7d9ddaa..20e0d7885b 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,27 @@ # example-backend +## 0.2.54 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-backend@0.3.19 + - @backstage/plugin-tech-insights-backend@0.1.2 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.1 + - @backstage/plugin-auth-backend@0.4.9 + - @backstage/plugin-scaffolder-backend@0.15.14 + - @backstage/plugin-catalog-backend@0.18.0 + - @backstage/plugin-kafka-backend@0.2.12 + - @backstage/backend-common@0.9.11 + - @backstage/plugin-azure-devops-backend@0.2.2 + - @backstage/plugin-badges-backend@0.1.12 + - @backstage/plugin-code-coverage-backend@0.1.15 + - @backstage/plugin-jenkins-backend@0.1.8 + - @backstage/plugin-proxy-backend@0.2.14 + - @backstage/plugin-rollbar-backend@0.1.16 + - @backstage/plugin-search-backend@0.2.7 + - @backstage/plugin-techdocs-backend@0.10.9 + ## 0.2.52 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 4fb692f040..bf18728b9f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.52", + "version": "0.2.54", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,35 +24,35 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", "@backstage/integration": "^0.6.8", "@backstage/plugin-app-backend": "^0.3.19", - "@backstage/plugin-auth-backend": "^0.4.8", - "@backstage/plugin-azure-devops-backend": "^0.2.1", - "@backstage/plugin-badges-backend": "^0.1.11", - "@backstage/plugin-catalog-backend": "^0.17.4", - "@backstage/plugin-code-coverage-backend": "^0.1.14", + "@backstage/plugin-auth-backend": "^0.4.9", + "@backstage/plugin-azure-devops-backend": "^0.2.2", + "@backstage/plugin-badges-backend": "^0.1.12", + "@backstage/plugin-catalog-backend": "^0.18.0", + "@backstage/plugin-code-coverage-backend": "^0.1.15", "@backstage/plugin-graphql-backend": "^0.1.9", - "@backstage/plugin-jenkins-backend": "^0.1.7", - "@backstage/plugin-kubernetes-backend": "^0.3.18", - "@backstage/plugin-kafka-backend": "^0.2.11", - "@backstage/plugin-proxy-backend": "^0.2.13", - "@backstage/plugin-rollbar-backend": "^0.1.15", - "@backstage/plugin-scaffolder-backend": "^0.15.13", + "@backstage/plugin-jenkins-backend": "^0.1.8", + "@backstage/plugin-kubernetes-backend": "^0.3.19", + "@backstage/plugin-kafka-backend": "^0.2.12", + "@backstage/plugin-proxy-backend": "^0.2.14", + "@backstage/plugin-rollbar-backend": "^0.1.16", + "@backstage/plugin-scaffolder-backend": "^0.15.14", "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.7", - "@backstage/plugin-search-backend": "^0.2.6", + "@backstage/plugin-search-backend": "^0.2.7", "@backstage/plugin-search-backend-node": "^0.4.2", "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.5", "@backstage/plugin-search-backend-module-pg": "^0.2.1", - "@backstage/plugin-techdocs-backend": "^0.10.8", - "@backstage/plugin-tech-insights-backend": "^0.1.1", + "@backstage/plugin-techdocs-backend": "^0.10.9", + "@backstage/plugin-tech-insights-backend": "^0.1.2", "@backstage/plugin-tech-insights-node": "^0.1.0", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.0", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.1", "@backstage/plugin-todo-backend": "^0.1.13", - "@gitbeaker/node": "^30.2.0", + "@gitbeaker/node": "^34.6.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.3.1", @@ -68,7 +68,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index f978e84da9..4426ed7767 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,6 +33,7 @@ import { SingleHostDiscovery, UrlReaders, useHotMemoize, + ServerTokenManager, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; @@ -60,6 +61,7 @@ function makeCreateEnv(config: Config) { const root = getRootLogger(); const reader = UrlReaders.default({ logger: root, config }); const discovery = SingleHostDiscovery.fromConfig(config); + const tokenManager = ServerTokenManager.noop(); root.info(`Created UrlReader ${reader}`); @@ -70,7 +72,7 @@ function makeCreateEnv(config: Config) { const logger = root.child({ type: 'plugin', plugin }); const database = databaseManager.forPlugin(plugin); const cache = cacheManager.forPlugin(plugin); - return { logger, cache, database, config, reader, discovery }; + return { logger, cache, database, config, reader, discovery, tokenManager }; }; } diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 4cf9c10021..9a8db0f0f9 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -59,6 +59,7 @@ export default async function createPlugin({ discovery, config, database, + tokenManager, }: PluginEnvironment) { // Initialize a connection to a search engine. const searchEngine = await createSearchEngine({ config, logger, database }); @@ -68,7 +69,10 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { + discovery, + tokenManager, + }), }); indexBuilder.addCollator({ @@ -76,6 +80,7 @@ export default async function createPlugin({ collator: DefaultTechDocsCollator.fromConfig(config, { discovery, logger, + tokenManager, }), }); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 8290e569ef..4be9c036b3 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -20,6 +20,7 @@ import { PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, + TokenManager, UrlReader, } from '@backstage/backend-common'; @@ -30,4 +31,5 @@ export type PluginEnvironment = { config: Config; reader: UrlReader; discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; }; diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 407c4fcbf2..ed8525a2cc 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/cli +## 0.9.1 + +### Patch Changes + +- dde216acf4: Switch the default test coverage provider from the jest default one to `'v8'`, which provides much better coverage information when using the default Backstage test setup. This is considered a bug fix as the current coverage information is often very inaccurate. +- 719cc87d2f: Disable ES transforms in tests transformed by the `jestSucraseTransform.js`. This is not considered a breaking change since all code is already transpiled this way in the development setup. +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- ee055cf6db: Update the default routes to use id instead of title +- Updated dependencies + - @backstage/errors@0.1.5 + ## 0.9.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 1eed5a742c..1d40e2788c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.9.0", + "version": "0.9.1", "private": false, "publishConfig": { "access": "public" @@ -31,7 +31,7 @@ "@backstage/cli-common": "^0.1.6", "@backstage/config": "^0.1.11", "@backstage/config-loader": "^0.8.0", - "@backstage/errors": "^0.1.4", + "@backstage/errors": "^0.1.5", "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", @@ -90,7 +90,7 @@ "postcss": "^8.1.0", "process": "^0.11.10", "react": "^16.0.0", - "react-dev-utils": "^11.0.4", + "react-dev-utils": "^12.0.0-next.47", "react-hot-loader": "^4.12.21", "recursive-readdir": "^2.2.2", "replace-in-file": "^6.0.0", @@ -102,7 +102,7 @@ "rollup-pluginutils": "^2.8.2", "run-script-webpack-plugin": "^0.0.11", "semver": "^7.3.2", - "style-loader": "^1.2.1", + "style-loader": "^3.3.1", "sucrase": "^3.20.2", "tar": "^6.1.2", "terser-webpack-plugin": "^5.1.3", @@ -117,13 +117,13 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/core-app-api": "^0.1.21", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@backstage/theme": "^0.2.13", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index 22e7fb9de9..4fa3d1ac6e 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -20,8 +20,17 @@ import { buildBundle } from '../../lib/bundler'; import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; +import chalk from 'chalk'; export default async (cmd: Command) => { + if (cmd.lax) { + console.warn( + chalk.yellow( + `[DEPRECATED] - The --lax option is deprecated and will be removed in the future. Please open an issue towards https://github.com/backstage/backstage that describes your use-case if you need the flag to stay around.`, + ), + ); + } + const { name } = await fs.readJson(paths.resolveTarget('package.json')); await buildBundle({ entry: 'src/index', diff --git a/packages/cli/src/commands/backend/buildImage.ts b/packages/cli/src/commands/backend/buildImage.ts deleted file mode 100644 index b352e38203..0000000000 --- a/packages/cli/src/commands/backend/buildImage.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2020 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 { Command } from 'commander'; -import { yellow } from 'chalk'; -import fs from 'fs-extra'; -import { join as joinPath, relative as relativePath } from 'path'; -import { createDistWorkspace } from '../../lib/packager'; -import { paths } from '../../lib/paths'; -import { run } from '../../lib/run'; -import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; - -const PKG_PATH = 'package.json'; - -export default async (cmd: Command) => { - // Skip the preparation steps if we're being asked for help - if (cmd.args.includes('--help')) { - await run('docker', ['image', 'build', '--help']); - return; - } - - console.warn( - yellow(` -The backend:build-image command is deprecated and will be removed in the future. -Please use the backend:bundle command instead along with your own Docker setup. - - https://backstage.io/docs/deployment/docker -`), - ); - - const pkgPath = paths.resolveTarget(PKG_PATH); - const pkg = await fs.readJson(pkgPath); - const appConfigs = await findAppConfigs(); - const npmrc = (await fs.pathExists(paths.resolveTargetRoot('.npmrc'))) - ? ['.npmrc'] - : []; - const tempDistWorkspace = await createDistWorkspace([pkg.name], { - buildDependencies: Boolean(cmd.build), - files: [ - 'package.json', - 'yarn.lock', - ...npmrc, - ...appConfigs, - { src: paths.resolveTarget('Dockerfile'), dest: 'Dockerfile' }, - ], - parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), - skeleton: 'skeleton.tar', - }); - console.log(`Dist workspace ready at ${tempDistWorkspace}`); - - // all args are forwarded to docker build - await run('docker', ['image', 'build', '.', ...cmd.args], { - cwd: tempDistWorkspace, - }); - - await fs.remove(tempDistWorkspace); -}; - -/** - * Find all config files to copy into the image - */ -async function findAppConfigs(): Promise { - const files = []; - - for (const name of await fs.readdir(paths.targetRoot)) { - if (name.startsWith('app-config.') && name.endsWith('.yaml')) { - files.push(name); - } - } - - if (paths.targetRoot !== paths.targetDir) { - const dirPath = relativePath(paths.targetRoot, paths.targetDir); - - for (const name of await fs.readdir(paths.targetDir)) { - if (name.startsWith('app-config.') && name.endsWith('.yaml')) { - files.push(joinPath(dirPath, name)); - } - } - } - - return files; -} diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index a37d5e9ffe..4eb34823ce 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -30,7 +30,10 @@ export function registerCommands(program: CommanderStatic) { .command('app:build') .description('Build an app for a production release') .option('--stats', 'Write bundle stats to output directory') - .option('--lax', 'Do not require environment variables to be set') + .option( + '--lax', + '[DEPRECATED] - Do not require environment variables to be set', + ) .option(...configOption) .action(lazy(() => import('./app/build').then(m => m.default))); @@ -55,16 +58,6 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./backend/bundle').then(m => m.default))); - program - .command('backend:build-image') - .allowUnknownOption(true) - .helpOption(', --backstage-cli-help') // Let docker handle --help - .option('--build', 'Build packages before packing them into the image') - .description( - 'Bundles the package into a docker image. This command is deprecated and will be removed.', - ) - .action(lazy(() => import('./backend/buildImage').then(m => m.default))); - program .command('backend:dev') .description('Start local development server with HMR for the backend') @@ -115,7 +108,7 @@ export function registerCommands(program: CommanderStatic) { program .command('remove-plugin') - .description('Removes plugin in the current repository') + .description('[DEPRECATED] - Removes plugin in the current repository') .action( lazy(() => import('./remove-plugin/removePlugin').then(m => m.default)), ); diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index 11fdc8bb11..a82bfda36d 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -184,6 +184,12 @@ export const removeReferencesFromAppPackage = async ( }; export default async () => { + console.warn( + chalk.yellow( + '[DEPRECATED] - The remove-plugin command is deprecated and will be removed in the future.', + ), + ); + const questions: Question[] = [ { type: 'input', diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index bf0711ad6f..8c484fe2ac 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/codemods +## 0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.1.23 + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.1.22 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index bfb8d4d4b3..e95367a5ac 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.22", + "version": "0.1.23", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index b4c0350def..c275a1f045 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -41,10 +41,10 @@ "json-schema": "^0.4.0", "json-schema-merge-allof": "^0.8.1", "json-schema-traverse": "^1.0.0", + "node-fetch": "^2.6.1", "typescript-json-schema": "^0.51.0", "yaml": "^1.9.2", - "yup": "^0.32.9", - "node-fetch": "2.6.5" + "yup": "^0.32.9" }, "devDependencies": { "@types/jest": "^26.0.7", diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index b1d590a7bd..ad71ad6fe2 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/core-app-api +## 0.1.23 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- 000190de69: The `ApiRegistry` from `@backstage/core-app-api` class has been deprecated and will be removed in a future release. To replace it, we have introduced two new helpers that are exported from `@backstage/test-utils`, namely `TestApiProvider` and `TestApiRegistry`. + + These two new helpers are more tailored for writing tests and development setups, as they allow for partial implementations of each of the APIs. + + When migrating existing code it is typically best to prefer usage of `TestApiProvider` when possible, so for example the following code: + + ```tsx + render( + + {...} + + ) + ``` + + Would be migrated to this: + + ```tsx + render( + + {...} + + ) + ``` + + In cases where the `ApiProvider` is used in a more standalone way, for example to reuse a set of APIs across multiple tests, the `TestApiRegistry` can be used instead. Note that the `TestApiRegistry` only has a single static factory method, `.from()`, and it is slightly different from the existing `.from()` method on `ApiRegistry` in that it doesn't require the API pairs to be wrapped in an outer array. + + Usage that looks like this: + + ```ts + const apis = ApiRegistry.with( + identityApiRef, + mockIdentityApi as unknown as IdentityApi, + ).with(configApiRef, new ConfigReader({})); + ``` + + OR like this: + + ```ts + const apis = ApiRegistry.from([ + [identityApiRef, mockIdentityApi as unknown as IdentityApi], + [configApiRef, new ConfigReader({})], + ]); + ``` + + Would be migrated to this: + + ```ts + const apis = TestApiRegistry.from( + [identityApiRef, mockIdentityApi], + [configApiRef, new ConfigReader({})], + ); + ``` + + If your app is still using the `ApiRegistry` to construct the `apis` for `createApp`, we recommend that you move over to use the new method of supplying API factories instead, using `createApiFactory`. + +- Updated dependencies + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.1.22 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 96c021b1ee..6777088b68 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.22", + "version": "0.1.23", "private": false, "publishConfig": { "access": "public", @@ -30,9 +30,9 @@ }, "dependencies": { "@backstage/app-defaults": "^0.1.1", - "@backstage/core-components": "^0.7.4", + "@backstage/core-components": "^0.7.5", "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", @@ -47,8 +47,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 07401660e8..20872b26f0 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -349,6 +349,78 @@ describe('Integration Test', () => { }); }); + it('getFeatureFlags should return feature flags', async () => { + const storageFlags = new LocalStorageFeatureFlags(); + jest.spyOn(storageFlags, 'registerFlag'); + + const apis = [ + noOpAnalyticsApi, + createApiFactory({ + api: featureFlagsApiRef, + deps: { configApi: configApiRef }, + factory() { + return storageFlags; + }, + }), + ]; + + const app = new AppManager({ + apis, + defaultApis: [], + themes: [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + Provider: ({ children }) => <>{children}, + }, + ], + icons, + plugins: [ + createPlugin({ + id: 'test', + featureFlags: [ + { + name: 'foo', + }, + ], + register: p => p.featureFlags.register('name'), + }), + ], + components, + configLoader: async () => [], + bindRoutes: ({ bind }) => { + bind(plugin1.externalRoutes, { + extRouteRef1: plugin1RouteRef, + extRouteRef2: plugin2RouteRef, + }); + }, + }); + + const Provider = app.getProvider(); + const Router = app.getRouter(); + + await renderWithEffects( + + + + } /> + } /> + + + , + ); + + expect(storageFlags.registerFlag).toHaveBeenCalledWith({ + name: 'name', + pluginId: 'test', + }); + expect(storageFlags.registerFlag).toHaveBeenCalledWith({ + name: 'foo', + pluginId: 'test', + }); + }); + it('should track route changes via analytics api', async () => { const mockAnalyticsApi = new MockAnalyticsApi(); const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)]; diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 950f57bb7d..5903a8c874 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -272,17 +272,26 @@ export class AppManager implements BackstageApp { const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; for (const plugin of this.plugins.values()) { - for (const output of plugin.output()) { - switch (output.type) { - case 'feature-flag': { - featureFlagsApi.registerFlag({ - name: output.name, - pluginId: plugin.getId(), - }); - break; + if ('getFeatureFlags' in plugin) { + for (const flag of plugin.getFeatureFlags()) { + featureFlagsApi.registerFlag({ + name: flag.name, + pluginId: plugin.getId(), + }); + } + } else { + for (const output of plugin.output()) { + switch (output.type) { + case 'feature-flag': { + featureFlagsApi.registerFlag({ + name: output.name, + pluginId: plugin.getId(), + }); + break; + } + default: + break; } - default: - break; } } } diff --git a/packages/core-app-api/src/routing/RouteResolver.test.ts b/packages/core-app-api/src/routing/RouteResolver.test.ts index 1b7eb10003..b068bd5e6d 100644 --- a/packages/core-app-api/src/routing/RouteResolver.test.ts +++ b/packages/core-app-api/src/routing/RouteResolver.test.ts @@ -100,23 +100,55 @@ describe('RouteResolver', () => { expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined); }); - it('should resolve an absolute route and an app base path', () => { + it('should resolve an absolute route and sub route with an app base path', () => { const r = new RouteResolver( - new Map([[ref1, '/my-route']]), - new Map(), - [{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }], + new Map([ + [ref2, '/my-parent/:x'], + [ref1, '/my-route'], + ]), + new Map([[ref1, ref2]]), + [ + { + routeRefs: new Set([ref2]), + path: '/my-parent/:x', + ...rest, + children: [ + MATCH_ALL_ROUTE, + { routeRefs: new Set([ref1]), path: '/my-route', ...rest }, + ], + }, + ], new Map(), '/base', ); - expect(r.resolve(ref1, '/')?.()).toBe('/base/my-route'); - expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined); - expect(r.resolve(subRef1, '/')?.()).toBe('/base/my-route/foo'); - expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe( - '/base/my-route/foo/2a', + expect(r.resolve(ref1, '/my-parent/1x')?.()).toBe( + '/base/my-parent/1x/my-route', + ); + expect(r.resolve(ref1, '/base/my-parent/1x')?.()).toBe( + '/base/my-parent/1x/my-route', + ); + expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe('/base/my-parent/1x'); + expect(r.resolve(ref2, '/base')?.({ x: '1x' })).toBe('/base/my-parent/1x'); + expect(r.resolve(ref3, '/')?.({ y: '1y' })).toBe(undefined); + expect(r.resolve(subRef1, '/my-parent/2x')?.()).toBe( + '/base/my-parent/2x/my-route/foo', + ); + expect(r.resolve(subRef1, '/base/my-parent/2x')?.()).toBe( + '/base/my-parent/2x/my-route/foo', + ); + expect(r.resolve(subRef2, '/my-parent/3x')?.({ a: '2a' })).toBe( + '/base/my-parent/3x/my-route/foo/2a', + ); + expect(r.resolve(subRef2, '/base/my-parent/3x')?.({ a: '2a' })).toBe( + '/base/my-parent/3x/my-route/foo/2a', + ); + expect(r.resolve(subRef3, '/')?.({ x: '5x' })).toBe( + '/base/my-parent/5x/bar', + ); + expect(r.resolve(subRef4, '/')?.({ x: '6x', a: '4a' })).toBe( + '/base/my-parent/6x/bar/4a', ); - expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined); - expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined); expect(r.resolve(externalRef1, '/')?.()).toBe(undefined); expect(r.resolve(externalRef2, '/')?.()).toBe(undefined); expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined); diff --git a/packages/core-app-api/src/routing/RouteResolver.ts b/packages/core-app-api/src/routing/RouteResolver.ts index b5be5f893e..f186b28586 100644 --- a/packages/core-app-api/src/routing/RouteResolver.ts +++ b/packages/core-app-api/src/routing/RouteResolver.ts @@ -207,6 +207,20 @@ export class RouteResolver { return undefined; } + // The location that we get passed in uses the full path, so start by trimming off + // the app base path prefix in case we're running the app on a sub-path. + let relativeSourceLocation: Parameters[1]; + if (typeof sourceLocation === 'string') { + relativeSourceLocation = this.trimPath(sourceLocation); + } else if (sourceLocation.pathname) { + relativeSourceLocation = { + ...sourceLocation, + pathname: this.trimPath(sourceLocation.pathname), + }; + } else { + relativeSourceLocation = sourceLocation; + } + // Next we figure out the base path, which is the combination of the common parent path // between our current location and our target location, as well as the additional path // that is the difference between the parent path and the base of our target location. @@ -214,7 +228,7 @@ export class RouteResolver { this.appBasePath + resolveBasePath( targetRef, - sourceLocation, + relativeSourceLocation, this.routePaths, this.routeParents, this.routeObjects, @@ -225,4 +239,15 @@ export class RouteResolver { }; return routeFunc; } + + private trimPath(targetPath: string) { + if (!targetPath) { + return targetPath; + } + + if (targetPath.startsWith(this.appBasePath)) { + return targetPath.slice(this.appBasePath.length); + } + return targetPath; + } } diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index c62ba9e90d..b4898fa82b 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/core-components +## 0.7.5 + +### Patch Changes + +- 157530187a: Pin sidebar by default for easier navigation +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/core-plugin-api@0.2.1 + ## 0.7.4 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index afa9af0341..896295038c 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.7.4", + "version": "0.7.5", "private": false, "publishConfig": { "access": "public", @@ -30,8 +30,8 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/errors": "^0.1.4", + "@backstage/core-plugin-api": "^0.2.1", + "@backstage/errors": "^0.1.5", "@backstage/theme": "^0.2.13", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", @@ -67,9 +67,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.21", - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/core-app-api": "^0.1.23", + "@backstage/cli": "^0.9.1", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index a8d35e5a14..0022c99c68 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/core-plugin-api +## 0.2.1 + +### Patch Changes + +- 950b36393c: Deprecated `register` option of `createPlugin` and the `outputs` methods of the plugin instance. + + Introduces the `featureFlags` property to define your feature flags instead. + ## 0.2.0 ### Minor Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 27f3480930..560071e1fa 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -258,6 +258,7 @@ export type BackstagePlugin< getId(): string; output(): PluginOutput[]; getApis(): Iterable; + getFeatureFlags(): Iterable; provide(extension: Extension): T; routes: Routes; externalRoutes: ExternalRoutes; @@ -463,7 +464,7 @@ export type FeatureFlag = { pluginId: string; }; -// @public +// @public @deprecated export type FeatureFlagOutput = { type: 'feature-flag'; name: string; @@ -682,14 +683,20 @@ export type PluginConfig< register?(hooks: PluginHooks): void; routes?: Routes; externalRoutes?: ExternalRoutes; + featureFlags?: PluginFeatureFlagConfig[]; }; // @public +export type PluginFeatureFlagConfig = { + name: string; +}; + +// @public @deprecated export type PluginHooks = { featureFlags: FeatureFlagsHooks; }; -// @public +// @public @deprecated export type PluginOutput = FeatureFlagOutput; // @public diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index e5b4572dff..b6567dcb89 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.2.0", + "version": "0.2.1", "private": false, "publishConfig": { "access": "public", @@ -43,9 +43,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.21", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/src/plugin/Plugin.test.tsx b/packages/core-plugin-api/src/plugin/Plugin.test.tsx new file mode 100644 index 0000000000..d3758543a1 --- /dev/null +++ b/packages/core-plugin-api/src/plugin/Plugin.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { createPlugin } from './Plugin'; + +describe('Plugin Feature Flag', () => { + it('should be able to register and receive feature flags', () => { + expect( + createPlugin({ + id: 'test', + featureFlags: [{ name: 'test' }], + }).getFeatureFlags(), + ).toEqual([{ name: 'test' }]); + + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + }).getFeatureFlags(), + ).toEqual([{ name: 'blob' }]); + + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + featureFlags: [{ name: 'test' }], + }).getFeatureFlags(), + ).toEqual([{ name: 'test' }, { name: 'blob' }]); + + expect( + createPlugin({ + id: 'test', + }).getFeatureFlags(), + ).toEqual([]); + + /* deprecated tests */ + + expect( + createPlugin({ + id: 'test', + featureFlags: [{ name: 'test' }], + }).output(), + ).toEqual([{ name: 'test', type: 'feature-flag' }]); + + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + }).output(), + ).toEqual([{ name: 'blob', type: 'feature-flag' }]); + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + featureFlags: [{ name: 'test' }], + }).output(), + ).toEqual([ + { name: 'test', type: 'feature-flag' }, + { name: 'blob', type: 'feature-flag' }, + ]); + + expect( + createPlugin({ + id: 'test', + }).output(), + ).toEqual([]); + }); +}); diff --git a/packages/core-plugin-api/src/plugin/Plugin.tsx b/packages/core-plugin-api/src/plugin/Plugin.tsx index 52ca0e76e3..0fb7574cd7 100644 --- a/packages/core-plugin-api/src/plugin/Plugin.tsx +++ b/packages/core-plugin-api/src/plugin/Plugin.tsx @@ -21,6 +21,7 @@ import { Extension, AnyRoutes, AnyExternalRoutes, + PluginFeatureFlagConfig, } from './types'; import { AnyApiFactory } from '../apis'; @@ -44,6 +45,14 @@ export class PluginImpl< return this.config.apis ?? []; } + getFeatureFlags(): Iterable { + const registeredFlags = this.output() + .filter(({ type }) => type === 'feature-flag') + .map(({ name }) => ({ name })); + + return registeredFlags; + } + get routes(): Routes { return this.config.routes ?? ({} as Routes); } @@ -56,11 +65,18 @@ export class PluginImpl< if (this.storedOutput) { return this.storedOutput; } - if (!this.config.register) { - return []; + const outputs = new Array(); + this.storedOutput = outputs; + + if (this.config.featureFlags) { + for (const flag of this.config.featureFlags) { + outputs.push({ type: 'feature-flag', name: flag.name }); + } } - const outputs = new Array(); + if (!this.config.register) { + return outputs; + } this.config.register({ featureFlags: { @@ -70,7 +86,6 @@ export class PluginImpl< }, }); - this.storedOutput = outputs; return this.storedOutput; } diff --git a/packages/core-plugin-api/src/plugin/index.ts b/packages/core-plugin-api/src/plugin/index.ts index d3272607ef..4f7609fd9c 100644 --- a/packages/core-plugin-api/src/plugin/index.ts +++ b/packages/core-plugin-api/src/plugin/index.ts @@ -25,4 +25,5 @@ export type { PluginConfig, PluginHooks, PluginOutput, + PluginFeatureFlagConfig, } from './types'; diff --git a/packages/core-plugin-api/src/plugin/types.ts b/packages/core-plugin-api/src/plugin/types.ts index aeb7037c51..0f8d3404d8 100644 --- a/packages/core-plugin-api/src/plugin/types.ts +++ b/packages/core-plugin-api/src/plugin/types.ts @@ -19,7 +19,7 @@ import { AnyApiFactory } from '../apis/system'; /** * Replace with using {@link RouteRef}s. - * + * @deprecated will be removed * @public */ export type FeatureFlagOutput = { @@ -31,6 +31,7 @@ export type FeatureFlagOutput = { * {@link FeatureFlagOutput} type. * * @public + * @deprecated Use {@link BackstagePlugin.getFeatureFlags} instead. */ export type PluginOutput = FeatureFlagOutput; @@ -71,13 +72,30 @@ export type BackstagePlugin< ExternalRoutes extends AnyExternalRoutes = {}, > = { getId(): string; + /** + * @deprecated use getFeatureFlags instead. + * */ output(): PluginOutput[]; getApis(): Iterable; + /** + * Returns all registered feature flags for this plugin. + */ + getFeatureFlags(): Iterable; provide(extension: Extension): T; routes: Routes; externalRoutes: ExternalRoutes; }; +/** + * Plugin feature flag configuration. + * + * @public + */ +export type PluginFeatureFlagConfig = { + /** Feature flag name */ + name: string; +}; + /** * Plugin descriptor type. * @@ -89,14 +107,17 @@ export type PluginConfig< > = { id: string; apis?: Iterable; + /** @deprecated use featureFlags property instead for defining feature flags */ register?(hooks: PluginHooks): void; routes?: Routes; externalRoutes?: ExternalRoutes; + featureFlags?: PluginFeatureFlagConfig[]; }; /** * Holds hooks registered by the plugin. * + * @deprecated - feature flags are now registered in plugin config under featureFlags * @public */ export type PluginHooks = { diff --git a/packages/core-plugin-api/src/routing/RouteRef.ts b/packages/core-plugin-api/src/routing/RouteRef.ts index 26e63c8f3a..8bd3d604f6 100644 --- a/packages/core-plugin-api/src/routing/RouteRef.ts +++ b/packages/core-plugin-api/src/routing/RouteRef.ts @@ -59,21 +59,21 @@ export class RouteRefImpl if (config.path) { // eslint-disable-next-line no-console console.warn( - `[core-plugin-api] - routeRefs no longer decide their own path, please remove the path for ${this.toString()}. This will be removed in upcoming versions.`, + `DEPRECATION WARNING: Passing a path to createRouteRef is deprecated, please remove the path for ${this}.`, ); } if (config.icon) { // eslint-disable-next-line no-console console.warn( - `[core-plugin-api] - routeRefs no longer decide their own icon, please remove the icon for ${this.toString()}. This will be removed in upcoming versions.`, + `DEPRECATION WARNING: Passing an icon to createRouteRef is deprecated, please remove the icon for ${this}.`, ); } if (config.title) { // eslint-disable-next-line no-console console.warn( - `[core-plugin-api] - routeRefs no longer decide their own title, please remove the title for ${this.toString()}. This will be removed in upcoming versions.`, + `DEPRECATION WARNING: Passing a title to createRouteRef is deprecated, please remove the title for ${this}.`, ); } } diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index af1fd58ead..97a5ef9588 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/create-app +## 0.4.5 + +### Patch Changes + +- dcaeaac174: Cleaned out the `peerDependencies` in the published version of the package, making it much quicker to run `npx @backstage/create-app` as it no longer needs to install a long list of unnecessary. +- a5a5d7e1f1: DefaultTechDocsCollator is now included in the search backend, and the Search Page updated with the SearchType component that includes the techdocs type +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- 42ebbc18c0: Bump gitbeaker to the latest version + ## 0.4.4 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index d5ee616ff5..49f6e87376 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.4", + "version": "0.4.5", "private": false, "publishConfig": { "access": "public" @@ -25,6 +25,8 @@ "lint": "backstage-cli lint", "test": "backstage-cli test", "clean": "backstage-cli clean", + "prepack": "node scripts/prepack.js", + "postpack": "node scripts/postpack.js", "start": "nodemon --" }, "dependencies": { diff --git a/packages/create-app/scripts/postpack.js b/packages/create-app/scripts/postpack.js new file mode 100644 index 0000000000..f13db11e76 --- /dev/null +++ b/packages/create-app/scripts/postpack.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/* + * 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. + */ + +/* eslint-disable no-restricted-syntax */ + +const fs = require('fs-extra'); +const path = require('path'); + +async function main() { + const pkgPath = path.resolve(__dirname, '../package.json'); + const pkgBackupPath = path.resolve(__dirname, '../package.json-prepack'); + + try { + await fs.move(pkgBackupPath, pkgPath, { overwrite: true }); + } catch (err) { + console.error(`Failed to restore package.json during postpack, ${err}`); + } +} + +main().catch(err => { + console.error(err.stack); + process.exit(1); +}); diff --git a/packages/create-app/scripts/prepack.js b/packages/create-app/scripts/prepack.js new file mode 100644 index 0000000000..2caf865280 --- /dev/null +++ b/packages/create-app/scripts/prepack.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/* + * 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. + */ + +/* eslint-disable no-restricted-syntax */ + +const fs = require('fs-extra'); +const path = require('path'); + +async function main() { + const pkgPath = path.resolve(__dirname, '../package.json'); + const pkgBackupPath = path.resolve(__dirname, '../package.json-prepack'); + + const pkg = await fs.readJson(pkgPath); + await fs.writeJson(pkgBackupPath, pkg, { encoding: 'utf8', spaces: 2 }); + delete pkg.peerDependencies; + await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 }); +} + +main().catch(err => { + console.error(err.stack); + process.exit(1); +}); diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index ab4858edd6..be144d91f7 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -6,6 +6,11 @@ organization: name: My Company backend: + # Used for enabling authentication, secret is shared by all backend plugins + # See backend-to-backend-auth.md in the docs for information on the format + # auth: + # keys: + # - secret: ${BACKEND_SECRET} baseUrl: http://localhost:7007 listen: port: 7007 diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 8f2de6a8ae..6f0726b0f8 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -27,7 +27,7 @@ "@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}", "@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", - "@gitbeaker/node": "^30.2.0", + "@gitbeaker/node": "^34.6.0", "@octokit/rest": "^18.5.3", "dockerode": "^3.3.1", "express": "^4.17.1", diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index f2b14b23f9..3f12122a3f 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -17,6 +17,7 @@ import { DatabaseManager, SingleHostDiscovery, UrlReaders, + ServerTokenManager, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import app from './plugins/app'; @@ -37,12 +38,13 @@ function makeCreateEnv(config: Config) { const cacheManager = CacheManager.fromConfig(config); const databaseManager = DatabaseManager.fromConfig(config); + const tokenManager = ServerTokenManager.noop(); return (plugin: string): PluginEnvironment => { const logger = root.child({ type: 'plugin', plugin }); const database = databaseManager.forPlugin(plugin); const cache = cacheManager.forPlugin(plugin); - return { logger, database, cache, config, reader, discovery }; + return { logger, database, cache, config, reader, discovery, tokenManager }; }; } diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts index 63e196235c..f23b0c7bcf 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts @@ -12,6 +12,7 @@ export default async function createPlugin({ logger, discovery, config, + tokenManager, }: PluginEnvironment) { // Initialize a connection to a search engine. const searchEngine = new LunrSearchEngine({ logger }); @@ -21,13 +22,20 @@ export default async function createPlugin({ // collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { + discovery, + tokenManager, + }), }); // collator gathers entities from techdocs. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { discovery, logger }), + collator: DefaultTechDocsCollator.fromConfig(config, { + discovery, + logger, + tokenManager, + }), }); // The scheduler controls when documents are gathered from collators and sent diff --git a/packages/create-app/templates/default-app/packages/backend/src/types.ts b/packages/create-app/templates/default-app/packages/backend/src/types.ts index 6c78a2a90c..b1e2e0a1df 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/types.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/types.ts @@ -4,6 +4,7 @@ import { PluginCacheManager, PluginDatabaseManager, PluginEndpointDiscovery, + TokenManager, UrlReader, } from '@backstage/backend-common'; @@ -14,4 +15,5 @@ export type PluginEnvironment = { config: Config; reader: UrlReader; discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; }; diff --git a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx index 3907dc1007..43eaa6218c 100644 --- a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx +++ b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; + import { AppThemeApi, appThemeApiRef } from '@backstage/core-plugin-api'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { BackstageTheme } from '@backstage/theme'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -24,7 +24,6 @@ import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; describe('SidebarThemeSwitcher', () => { let appThemeApi: jest.Mocked; - let apiRegistry: ApiRegistry; beforeEach(() => { appThemeApi = { @@ -51,15 +50,13 @@ describe('SidebarThemeSwitcher', () => { theme: {} as unknown as BackstageTheme, }, ]); - - apiRegistry = ApiRegistry.with(appThemeApiRef, appThemeApi); }); it('should display current theme', async () => { const { getByLabelText, getByRole, getByText } = await renderInTestApp( - + - , + , ); const button = getByLabelText('Switch Theme'); @@ -76,9 +73,9 @@ describe('SidebarThemeSwitcher', () => { it('should select different theme', async () => { const { getByLabelText, getByRole, getByText } = await renderInTestApp( - + - , + , ); const button = getByLabelText('Switch Theme'); diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index 9ea2f47cbb..02b28cdf1d 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/errors +## 0.1.5 + +### Patch Changes + +- 4d09c60256: Deprecate `parseErrorResponse` in favour of `parseErrorResponseBody`. Deprecate `data` field inside `ErrorResponse` in favour of `body`. + Rename the error name for unknown errors from `unknown` to `error`. + ## 0.1.4 ### Patch Changes diff --git a/packages/errors/package.json b/packages/errors/package.json index 38fe6c634e..f49451a6a8 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/errors", "description": "Common utilities for error handling within Backstage", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 4c2258779a..fcaa39b285 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -112,7 +112,9 @@ export function getAzureFileFetchUrl(url: string): string; export function getAzureRequestOptions( config: AzureIntegrationConfig, additionalHeaders?: Record, -): RequestInit; +): { + headers: Record; +}; // @public export function getBitbucketDefaultBranch( @@ -135,7 +137,9 @@ export function getBitbucketFileFetchUrl( // @public export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, -): RequestInit; +): { + headers: Record; +}; // @public export function getGitHubFileFetchUrl( @@ -148,7 +152,9 @@ export function getGitHubFileFetchUrl( export function getGitHubRequestOptions( config: GitHubIntegrationConfig, credentials: GithubCredentials, -): RequestInit; +): { + headers: Record; +}; // @public export function getGitLabFileFetchUrl( @@ -157,9 +163,9 @@ export function getGitLabFileFetchUrl( ): Promise; // @public -export function getGitLabRequestOptions( - config: GitLabIntegrationConfig, -): RequestInit; +export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { + headers: Record; +}; // @public export type GithubAppConfig = { diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 7461954267..1d5c4c3c89 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -64,8 +64,8 @@ export function getAzureCommitsUrl(url: string): string { export function getAzureRequestOptions( config: AzureIntegrationConfig, additionalHeaders?: Record, -): RequestInit { - const headers: HeadersInit = additionalHeaders +): { headers: Record } { + const headers: Record = additionalHeaders ? { ...additionalHeaders } : {}; diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index afcb9fcfdb..da10152bf8 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -158,8 +158,8 @@ export function getBitbucketFileFetchUrl( */ export function getBitbucketRequestOptions( config: BitbucketIntegrationConfig, -): RequestInit { - const headers: HeadersInit = {}; +): { headers: Record } { + const headers: Record = {}; if (config.token) { headers.Authorization = `Bearer ${config.token}`; diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts index 2c5e739a26..76e54fd544 100644 --- a/packages/integration/src/github/core.ts +++ b/packages/integration/src/github/core.ts @@ -73,8 +73,8 @@ export function getGitHubFileFetchUrl( export function getGitHubRequestOptions( config: GitHubIntegrationConfig, credentials: GithubCredentials, -): RequestInit { - const headers: HeadersInit = {}; +): { headers: Record } { + const headers: Record = {}; if (chooseEndpoint(config, credentials) === 'api') { headers.Accept = 'application/vnd.github.v3.raw'; diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index e6f4dbc910..c5b0713340 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -54,9 +54,9 @@ export async function getGitLabFileFetchUrl( * @param config - The relevant provider config * @public */ -export function getGitLabRequestOptions( - config: GitLabIntegrationConfig, -): RequestInit { +export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { + headers: Record; +} { const { token = '' } = config; return { headers: { diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index bc4377c794..4c0813a8e9 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -1,6 +1,5 @@ import { AlertApiForwarder, - ApiRegistry, ErrorAlerter, ErrorApiForwarder, GithubAuth, @@ -29,80 +28,59 @@ import { featureFlagsApiRef, } from '@backstage/core-plugin-api'; -const builder = ApiRegistry.builder(); - -builder.add(featureFlagsApiRef, new LocalStorageFeatureFlags()); - -builder.add(configApiRef, new ConfigReader({})); - -const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); - -builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); - -builder.add(identityApiRef, { +const configApi = new ConfigReader({}); +const featureFlagsApi = new LocalStorageFeatureFlags(); +const alertApi = new AlertApiForwarder(); +const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); +const identityApi = { getUserId: () => 'guest', getProfile: () => ({ email: 'guest@example.com' }), getIdToken: () => undefined, signOut: async () => {}, +}; +const oauthRequestApi = new OAuthRequestManager(); +const googleAuthApi = GoogleAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const githubAuthApi = GithubAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const gitlabAuthApi = GitlabAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const oktaAuthApi = OktaAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const auth0AuthApi = Auth0Auth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const oauth2Api = OAuth2.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, }); -const oauthRequestApi = builder.add( - oauthRequestApiRef, - new OAuthRequestManager(), -); - -builder.add( - googleAuthApiRef, - GoogleAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - githubAuthApiRef, - GithubAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - gitlabAuthApiRef, - GitlabAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - oktaAuthApiRef, - OktaAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - auth0AuthApiRef, - Auth0Auth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - oauth2ApiRef, - OAuth2.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -export const apis = builder.build(); +export const apis = [ + [configApiRef, configApi], + [featureFlagsApiRef, featureFlagsApi], + [alertApiRef, alertApi], + [errorApiRef, errorApi], + [identityApiRef, identityApi], + [oauthRequestApiRef, oauthRequestApi], + [googleAuthApiRef, googleAuthApi], + [githubAuthApiRef, githubAuthApi], + [gitlabAuthApiRef, gitlabAuthApi], + [oktaAuthApiRef, oktaAuthApi], + [auth0AuthApiRef, auth0AuthApi], + [oauth2ApiRef, oauth2Api], +]; diff --git a/packages/storybook/.storybook/preview.js b/packages/storybook/.storybook/preview.js index 6186b82ca5..2b1a4af329 100644 --- a/packages/storybook/.storybook/preview.js +++ b/packages/storybook/.storybook/preview.js @@ -6,17 +6,17 @@ import { useDarkMode } from 'storybook-dark-mode'; import { apis } from './apis'; import { Content, AlertDisplay } from '@backstage/core-components'; -import { ApiProvider } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; addDecorator(story => ( - + {story()} - + )); addParameters({ diff --git a/packages/techdocs-cli/.snyk b/packages/techdocs-cli/.snyk new file mode 100644 index 0000000000..cfb30e5aa7 --- /dev/null +++ b/packages/techdocs-cli/.snyk @@ -0,0 +1,17 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-BROWSERSLIST-1090194: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-05-20T00:00:00.000Z + created: 2021-11-20T00:00:00.000Z + + SNYK-JS-IMMER-1540542: + - '*': + reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code + expires: 2022-05-20T00:00:00.000Z + created: 2021-11-20T00:00:00.000Z + +patch: {} diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 70ca0a9668..05759e39d1 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -65,7 +65,7 @@ "dockerode": "^3.3.1", "fs-extra": "^9.0.1", "http-proxy": "^1.18.1", - "react-dev-utils": "^11.0.4", + "react-dev-utils": "^12.0.0-next.47", "serve-handler": "^6.1.3", "winston": "^3.2.1" } diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 0207ea409d..386c584196 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/techdocs-common +## 0.10.8 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + ## 0.10.7 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index b13b94a1a7..3f0d0edf5b 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.10.7", + "version": "0.10.8", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,10 +38,10 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/errors": "^0.1.4", + "@backstage/errors": "^0.1.5", "@backstage/search-common": "^0.2.1", "@backstage/integration": "^0.6.9", "@google-cloud/storage": "^5.6.0", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 3460e6c714..fd8ce852db 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/test-utils +## 0.1.23 + +### Patch Changes + +- 000190de69: The `ApiRegistry` from `@backstage/core-app-api` class has been deprecated and will be removed in a future release. To replace it, we have introduced two new helpers that are exported from `@backstage/test-utils`, namely `TestApiProvider` and `TestApiRegistry`. + + These two new helpers are more tailored for writing tests and development setups, as they allow for partial implementations of each of the APIs. + + When migrating existing code it is typically best to prefer usage of `TestApiProvider` when possible, so for example the following code: + + ```tsx + render( + + {...} + + ) + ``` + + Would be migrated to this: + + ```tsx + render( + + {...} + + ) + ``` + + In cases where the `ApiProvider` is used in a more standalone way, for example to reuse a set of APIs across multiple tests, the `TestApiRegistry` can be used instead. Note that the `TestApiRegistry` only has a single static factory method, `.from()`, and it is slightly different from the existing `.from()` method on `ApiRegistry` in that it doesn't require the API pairs to be wrapped in an outer array. + + Usage that looks like this: + + ```ts + const apis = ApiRegistry.with( + identityApiRef, + mockIdentityApi as unknown as IdentityApi, + ).with(configApiRef, new ConfigReader({})); + ``` + + OR like this: + + ```ts + const apis = ApiRegistry.from([ + [identityApiRef, mockIdentityApi as unknown as IdentityApi], + [configApiRef, new ConfigReader({})], + ]); + ``` + + Would be migrated to this: + + ```ts + const apis = TestApiRegistry.from( + [identityApiRef, mockIdentityApi], + [configApiRef, new ConfigReader({})], + ); + ``` + + If your app is still using the `ApiRegistry` to construct the `apis` for `createApp`, we recommend that you move over to use the new method of supplying API factories instead, using `createApiFactory`. + +- Updated dependencies + - @backstage/core-app-api@0.1.23 + - @backstage/core-plugin-api@0.2.1 + ## 0.1.22 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 79667c236a..7e684adbfc 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.22", + "version": "0.1.23", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.21", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-app-api": "^0.1.23", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", @@ -46,7 +46,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index 25809912ec..39e5da3df2 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { createExternalRouteRef, createRouteRef, @@ -29,6 +28,7 @@ import React, { useEffect } from 'react'; import { Route, Routes } from 'react-router'; import { MockErrorApi } from './apis'; import { renderInTestApp, wrapInTestApp } from './appWrappers'; +import { TestApiProvider } from './TestApiProvider'; describe('wrapInTestApp', () => { it('should provide routing and warn about missing act()', async () => { @@ -111,9 +111,9 @@ describe('wrapInTestApp', () => { }; const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('foo')).toBeInTheDocument(); diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 9efe6132e8..33fc4db9d9 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -23,8 +23,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -36,10 +36,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index e0a32960c5..ffd6dd364c 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -34,10 +34,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 0b0bd4890c..c880dc983c 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-api-docs +## 0.6.15 + +### Patch Changes + +- c982fc12cb: Adjusted some styles in the OpenAPI definition, for elements which were barely readable in dark mode. +- Updated dependencies + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.6.14 ### Patch Changes diff --git a/plugins/api-docs/README.md b/plugins/api-docs/README.md index d5ed198dc1..4919690568 100644 --- a/plugins/api-docs/README.md +++ b/plugins/api-docs/README.md @@ -160,6 +160,14 @@ by this plugin. Grab a copy of [oauth2-redirect.html](https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html) and put it in the `app/public/` directory in order to enable Swagger UI to complete this redirection. +This also may require you to adjust `Content Security Policy` header settings of your Backstage application, so that the script in `oauth2-redirect.html` can be executed. Since the script is static we can add the hash of it directly to our CSP policy, which we do by adding the following to the `csp` section of the app configuration: + +```yaml +script-src: + - "'self'" + - "'sha256-GeDavzSZ8O71Jggf/pQkKbt52dfZkrdNMQ3e+Ox+AkI='" # oauth2-redirect.html +``` + #### Configuring your OAuth2 Client You'll need to make sure your OAuth2 client has been registered in your OAuth2 Authentication Server (AS) diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index a4aa15c419..9f8b5fbf84 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.6.14", + "version": "0.6.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ "dependencies": { "@asyncapi/react-component": "^0.23.0", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog": "^0.7.3", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", @@ -53,10 +53,10 @@ "swagger-ui-react": "^4.0.0-rc.3" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx index 7b0ffac58e..32a6ec0f77 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx @@ -16,13 +16,12 @@ import { ApiEntity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; import { ApiDefinitionCard } from './ApiDefinitionCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -31,10 +30,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx index 450e9e57ac..5476857a06 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx @@ -15,11 +15,10 @@ */ import { ApiEntity } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { ApiTypeTitle } from './ApiTypeTitle'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -28,10 +27,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index cf40e5160c..3629110d47 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -15,11 +15,7 @@ */ import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { ConfigApi, @@ -34,7 +30,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; import DashboardIcon from '@material-ui/icons/Dashboard'; import { render } from '@testing-library/react'; import React from 'react'; @@ -88,8 +88,8 @@ describe('ApiCatalogPage', () => { const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( - { new DefaultStarredEntitiesApi({ storageApi }), ], [apiDocsConfigRef, apiDocsConfig], - ])} + ]} > {children} - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index ac09b717f7..5e7b22524a 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -21,12 +21,11 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { ConsumedApisCard } from './ConsumedApisCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -43,13 +42,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( - apiDocsConfigRef, - apiDocsConfig, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx index ac833251e3..4102bebf0e 100644 --- a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx @@ -21,12 +21,11 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { HasApisCard } from './HasApisCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -43,13 +42,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( - apiDocsConfigRef, - apiDocsConfig, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index f2719d0f4b..b29075aee9 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -21,12 +21,11 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { ProvidedApisCard } from './ProvidedApisCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -43,13 +42,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( - apiDocsConfigRef, - apiDocsConfig, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index 274962c346..06022eec69 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -21,11 +21,10 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ConsumingComponentsCard } from './ConsumingComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -39,10 +38,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index 879944749a..275338d2f9 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -21,11 +21,10 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ProvidingComponentsCard } from './ProvidingComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -39,10 +38,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx index 4a74b238f7..8d0accb254 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx @@ -61,12 +61,13 @@ const useStyles = makeStyles(theme => ({ [`& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, - .opblock .opblock-section-header h4, + .opblock h4, + .opblock h5, + .opblock a, + .opblock li, .parameter__name, .response-col_status, .response-col_links, - .responses-inner h4, - .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, @@ -89,6 +90,7 @@ const useStyles = makeStyles(theme => ({ color: theme.palette.text.disabled, }, [`& .opblock-description-wrapper p, + .opblock-description-wrapper li, .opblock-external-docs-wrapper p, .opblock-title_normal p, .response-control-media-type__accept-message, diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 5dc0cdb861..38eaf268e6 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-auth-backend +## 0.4.9 + +### Patch Changes + +- 9312572360: Switched to using the standardized JSON error responses for all provider endpoints. +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + - @backstage/test-utils@0.1.23 + ## 0.4.8 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 35c93e9537..eb7c53bd6b 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -175,6 +175,20 @@ export const bitbucketUserIdSignInResolver: SignInResolver // @public (undocumented) export const bitbucketUsernameSignInResolver: SignInResolver; +// Warning: (ae-missing-release-tag) "CatalogIdentityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class CatalogIdentityClient { + constructor(options: { catalogApi: CatalogApi; tokenIssuer: TokenIssuer }); + // Warning: (ae-forgotten-export) The symbol "UserQuery" needs to be exported by the entry point index.d.ts + findUser(query: UserQuery): Promise; + // Warning: (ae-forgotten-export) The symbol "MemberClaimQuery" needs to be exported by the entry point index.d.ts + resolveCatalogMembership({ + entityRefs, + logger, + }: MemberClaimQuery): Promise; +} + // Warning: (ae-missing-release-tag) "createAtlassianProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -271,6 +285,12 @@ export const encodeState: (state: OAuthState) => string; // @public (undocumented) export const ensuresXRequestedWith: (req: express.Request) => boolean; +// Warning: (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "getEntityClaims" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function getEntityClaims(entity: UserEntity): TokenParams['claims']; + // Warning: (ae-missing-release-tag) "GithubOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -578,7 +598,6 @@ export type WebMessageResponse = // Warnings were encountered during analysis: // -// src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts // src/providers/atlassian/provider.d.ts:37:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts // src/providers/atlassian/provider.d.ts:42:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 3a9e8102eb..fecabf8ad1 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.4.8", + "version": "0.4.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,19 +30,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/errors": "^0.1.4", - "@backstage/test-utils": "^0.1.22", + "@backstage/errors": "^0.1.5", + "@backstage/test-utils": "^0.1.23", "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "cors": "^2.8.5", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-session": "^1.17.1", @@ -56,6 +55,7 @@ "luxon": "^2.0.2", "minimatch": "^3.0.3", "morgan": "^1.10.0", + "node-fetch": "^2.6.1", "node-cache": "^5.1.2", "openid-client": "^4.2.1", "passport": "^0.4.1", @@ -73,7 +73,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-backend/src/identity/IdentityClient.ts index 2af30e4b6a..d60829bf59 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; +import fetch from 'node-fetch'; import { JWK, JWT, JWKS, JSONWebKey } from 'jose'; import { BackstageIdentity } from '../providers'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 432da00142..894f8c1e55 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -31,3 +31,5 @@ export * from './lib/flow'; // OAuth wrapper over a passport or a custom `startegy`. export * from './lib/oauth'; + +export * from './lib/catalog'; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 2f5ffa1833..3f72004d48 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -50,7 +50,7 @@ const mockClaims = { }; jest.mock('jose'); -jest.mock('cross-fetch', () => ({ +jest.mock('node-fetch', () => ({ __esModule: true, default: async () => { return { diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index ac5db5e69f..b0b9070d50 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -21,7 +21,7 @@ import { SignInResolver, } from '../types'; import express from 'express'; -import fetch from 'cross-fetch'; +import fetch from 'node-fetch'; import * as crypto from 'crypto'; import { KeyObject } from 'crypto'; import { Logger } from 'winston'; diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 06fc4833c4..26c64b3f9d 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-azure-devops-backend +## 0.2.2 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/backend-common@0.9.11 + ## 0.2.1 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 6d213cbce6..12f62355dd 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/config": "^0.1.11", "@backstage/plugin-azure-devops-common": "^0.1.0", "@types/express": "^4.17.6", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.35.0" diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 71f0b8b807..61361495b3 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -27,6 +27,108 @@ export enum BuildStatus { Postponed = 8, } +// Warning: (ae-missing-release-tag) "CreatedBy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface CreatedBy { + // (undocumented) + displayName?: string; + // (undocumented) + id?: string; + // (undocumented) + imageUrl?: string; + // (undocumented) + uniqueName?: string; +} + +// Warning: (ae-missing-release-tag) "DashboardPullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface DashboardPullRequest { + // (undocumented) + createdBy?: CreatedBy; + // (undocumented) + creationDate?: string; + // (undocumented) + description?: string; + // (undocumented) + hasAutoComplete: boolean; + // (undocumented) + isDraft?: boolean; + // (undocumented) + link?: string; + // (undocumented) + policies?: Policy[]; + // (undocumented) + pullRequestId?: number; + // (undocumented) + repository?: Repository; + // (undocumented) + reviewers?: Reviewer[]; + // (undocumented) + status?: PullRequestStatus; + // (undocumented) + title?: string; +} + +// Warning: (ae-missing-release-tag) "Policy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface Policy { + // (undocumented) + id?: number; + // (undocumented) + link?: string; + // (undocumented) + status?: PolicyEvaluationStatus; + // (undocumented) + text?: string; + // (undocumented) + type: PolicyType; +} + +// Warning: (ae-missing-release-tag) "PolicyEvaluationStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export enum PolicyEvaluationStatus { + Approved = 2, + Broken = 5, + NotApplicable = 4, + Queued = 0, + Rejected = 3, + Running = 1, +} + +// Warning: (ae-missing-release-tag) "PolicyType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum PolicyType { + // (undocumented) + Build = 'Build', + // (undocumented) + Comments = 'Comments', + // (undocumented) + MergeStrategy = 'MergeStrategy', + // (undocumented) + MinimumReviewers = 'MinimumReviewers', + // (undocumented) + RequiredReviewers = 'RequiredReviewers', + // (undocumented) + Status = 'Status', +} + +// Warning: (ae-missing-release-tag) "PolicyTypeId" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const PolicyTypeId: { + Build: string; + Status: string; + MinimumReviewers: string; + Comments: string; + RequiredReviewers: string; + MergeStrategy: string; +}; + // Warning: (ae-missing-release-tag) "PullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -63,6 +165,22 @@ export enum PullRequestStatus { NotSet = 0, } +// Warning: (ae-missing-release-tag) "PullRequestVoteStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum PullRequestVoteStatus { + // (undocumented) + Approved = 10, + // (undocumented) + ApprovedWithSuggestions = 5, + // (undocumented) + NoVote = 0, + // (undocumented) + Rejected = -10, + // (undocumented) + WaitingForAuthor = -5, +} + // Warning: (ae-missing-release-tag) "RepoBuild" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -86,5 +204,47 @@ export type RepoBuildOptions = { top?: number; }; +// Warning: (ae-missing-release-tag) "Repository" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface Repository { + // (undocumented) + id?: string; + // (undocumented) + name?: string; + // (undocumented) + url?: string; +} + +// Warning: (ae-missing-release-tag) "Reviewer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface Reviewer { + // (undocumented) + displayName?: string; + // (undocumented) + id?: string; + // (undocumented) + imageUrl?: string; + // (undocumented) + isContainer?: boolean; + // (undocumented) + isRequired?: boolean; + // (undocumented) + voteStatus: PullRequestVoteStatus; +} + +// Warning: (ae-missing-release-tag) "Team" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface Team { + // (undocumented) + id?: string; + // (undocumented) + memberIds?: string[]; + // (undocumented) + name?: string; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index a647f87300..844001771f 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -126,3 +126,128 @@ export type PullRequestOptions = { top: number; status: PullRequestStatus; }; + +export interface DashboardPullRequest { + pullRequestId?: number; + title?: string; + description?: string; + repository?: Repository; + createdBy?: CreatedBy; + hasAutoComplete: boolean; + policies?: Policy[]; + reviewers?: Reviewer[]; + creationDate?: string; + status?: PullRequestStatus; + isDraft?: boolean; + link?: string; +} + +export interface Reviewer { + id?: string; + displayName?: string; + imageUrl?: string; + isRequired?: boolean; + isContainer?: boolean; + voteStatus: PullRequestVoteStatus; +} + +export interface Policy { + id?: number; + type: PolicyType; + status?: PolicyEvaluationStatus; + text?: string; + link?: string; +} + +export interface CreatedBy { + id?: string; + displayName?: string; + uniqueName?: string; + imageUrl?: string; +} + +export interface Repository { + id?: string; + name?: string; + url?: string; +} + +export interface Team { + id?: string; + name?: string; + memberIds?: string[]; +} + +/** + * Status of a policy which is running against a specific pull request. + */ +export enum PolicyEvaluationStatus { + /** + * The policy is either queued to run, or is waiting for some event before progressing. + */ + Queued = 0, + /** + * The policy is currently running. + */ + Running = 1, + /** + * The policy has been fulfilled for this pull request. + */ + Approved = 2, + /** + * The policy has rejected this pull request. + */ + Rejected = 3, + /** + * The policy does not apply to this pull request. + */ + NotApplicable = 4, + /** + * The policy has encountered an unexpected error. + */ + Broken = 5, +} + +export enum PolicyType { + Build = 'Build', + Status = 'Status', + MinimumReviewers = 'MinimumReviewers', + Comments = 'Comments', + RequiredReviewers = 'RequiredReviewers', + MergeStrategy = 'MergeStrategy', +} + +export const PolicyTypeId = { + /** + * This policy will require a successful build has been performed before updating protected refs. + */ + Build: '0609b952-1397-4640-95ec-e00a01b2c241', + /** + * This policy will require a successful status to be posted before updating protected refs. + */ + Status: 'cbdc66da-9728-4af8-aada-9a5a32e4a226', + /** + * This policy will ensure that a minimum number of reviewers have approved a pull request before completion. + */ + MinimumReviewers: 'fa4e907d-c16b-4a4c-9dfa-4906e5d171dd', + /** + * Check if the pull request has any active comments. + */ + Comments: 'c6a1889d-b943-4856-b76f-9e46bb6b0df2', + /** + * This policy will ensure that required reviewers are added for modified files matching specified patterns. + */ + RequiredReviewers: 'fd2167ab-b0be-447a-8ec8-39368250530e', + /** + * This policy ensures that pull requests use a consistent merge strategy. + */ + MergeStrategy: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab', +}; + +export enum PullRequestVoteStatus { + Approved = 10, + ApprovedWithSuggestions = 5, + NoVote = 0, + WaitingForAuthor = -5, + Rejected = -10, +} diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index ab659534c1..cebe6595f7 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -7,12 +7,23 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { SvgIconProps } from '@material-ui/core'; // Warning: (ae-missing-release-tag) "azureDevOpsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const azureDevOpsPlugin: BackstagePlugin<{}, {}>; +// Warning: (ae-missing-release-tag) "AzurePullRequestsIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element; + +// Warning: (ae-missing-release-tag) "AzurePullRequestsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const AzurePullRequestsPage: () => JSX.Element; + // Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 9d235b0641..2badcf63b5 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -28,8 +28,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.4", "@backstage/plugin-azure-devops-common": "^0.1.0", "@backstage/plugin-catalog-react": "^0.6.4", @@ -45,10 +45,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts index 90d291b25d..80d36f0adf 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts @@ -15,10 +15,12 @@ */ import { + DashboardPullRequest, PullRequest, PullRequestOptions, RepoBuild, RepoBuildOptions, + Team, } from '@backstage/plugin-azure-devops-common'; import { createApiRef } from '@backstage/core-plugin-api'; @@ -41,4 +43,10 @@ export interface AzureDevOpsApi { repoName: string, options?: PullRequestOptions, ): Promise<{ items: PullRequest[] }>; + + getDashboardPullRequests( + projectName: string, + ): Promise; + + getAllTeams(): Promise; } diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts index cbcc496725..28d7e19cff 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -14,13 +14,15 @@ * limitations under the License. */ -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { + DashboardPullRequest, PullRequest, PullRequestOptions, RepoBuild, RepoBuildOptions, + Team, } from '@backstage/plugin-azure-devops-common'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { AzureDevOpsApi } from './AzureDevOpsApi'; import { ResponseError } from '@backstage/errors'; @@ -29,7 +31,7 @@ export class AzureDevOpsClient implements AzureDevOpsApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; - constructor(options: { + public constructor(options: { discoveryApi: DiscoveryApi; identityApi: IdentityApi; }) { @@ -74,6 +76,18 @@ export class AzureDevOpsClient implements AzureDevOpsApi { return { items }; } + public getDashboardPullRequests( + projectName: string, + ): Promise { + return this.get( + `dashboard-pull-requests/${projectName}?top=100`, + ); + } + + public getAllTeams(): Promise { + return this.get('all-teams'); + } + private async get(path: string): Promise { const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`; const url = new URL(path, baseUrl); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx new file mode 100644 index 0000000000..c3f0536749 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx @@ -0,0 +1,97 @@ +/* + * 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 { + Content, + Header, + Page, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { PullRequestGroup, PullRequestGroupConfig } from './lib/types'; +import React, { useEffect, useState } from 'react'; +import { getCreatedByUserFilter, getPullRequestGroups } from './lib/utils'; +import { useDashboardPullRequests, useUserEmail } from '../../hooks'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { PullRequestGrid } from './lib/PullRequestGrid'; + +/** + * @deprecated TEMPORARY - This will be configurable in a follow up PR. + */ +const PROJECT_NAME = 'projectName'; + +function usePullRequestGroupConfigs( + userEmail: string | undefined, +): PullRequestGroupConfig[] { + const [pullRequestGroupConfigs, setPullRequestGroupConfigs] = useState< + PullRequestGroupConfig[] + >([]); + + useEffect(() => { + const prGroupConfigs: PullRequestGroupConfig[] = [ + { title: 'Created by me', filter: getCreatedByUserFilter(userEmail) }, + { title: 'Other PRs', filter: _ => true, simplified: false }, + ]; + + setPullRequestGroupConfigs(prGroupConfigs); + }, [userEmail]); + + return pullRequestGroupConfigs; +} + +function usePullRequestGroups( + pullRequests: DashboardPullRequest[] | undefined, + pullRequestGroupConfigs: PullRequestGroupConfig[], +): PullRequestGroup[] { + const [pullRequestGroups, setPullRequestGroups] = useState< + PullRequestGroup[] + >([]); + + useEffect(() => { + if (pullRequests) { + const groups = getPullRequestGroups( + pullRequests, + pullRequestGroupConfigs, + ); + setPullRequestGroups(groups); + } + }, [pullRequests, pullRequestGroupConfigs]); + + return pullRequestGroups; +} + +export const PullRequestsPage = () => { + const { pullRequests, error } = useDashboardPullRequests(PROJECT_NAME); + const userEmail = useUserEmail(); + const pullRequestGroupConfigs = usePullRequestGroupConfigs(userEmail); + const pullRequestGroups = usePullRequestGroups( + pullRequests, + pullRequestGroupConfigs, + ); + + const pullRequestsContent = error ? ( + + ) : ( + + ); + + return ( + +
+ {pullRequestsContent} + + ); +}; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/index.ts new file mode 100644 index 0000000000..e8b6cfa6bb --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { PullRequestsPage } from './PullRequestsPage'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/AutoCompleteIcon/AutoCompleteIcon.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/AutoCompleteIcon/AutoCompleteIcon.tsx new file mode 100644 index 0000000000..ca43b8e27b --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/AutoCompleteIcon/AutoCompleteIcon.tsx @@ -0,0 +1,32 @@ +/* + * 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 DoneAllIcon from '@material-ui/icons/DoneAll'; +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles(theme => ({ + root: (props: { hasAutoComplete: boolean }) => ({ + color: props.hasAutoComplete + ? theme.palette.success.main + : theme.palette.grey[400], + }), +})); + +export const AutoCompleteIcon = (props: { hasAutoComplete: boolean }) => { + const classes = useStyles(props); + return ; +}; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/AutoCompleteIcon/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/AutoCompleteIcon/index.ts new file mode 100644 index 0000000000..d83b0ffad0 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/AutoCompleteIcon/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { AutoCompleteIcon } from './AutoCompleteIcon'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.stories.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.stories.tsx new file mode 100644 index 0000000000..b948b616bd --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.stories.tsx @@ -0,0 +1,118 @@ +/* + * 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 { + DashboardPullRequest, + PolicyEvaluationStatus, + PolicyType, + PullRequestStatus, + PullRequestVoteStatus, +} from '@backstage/plugin-azure-devops-common'; + +import { MemoryRouter } from 'react-router'; +import { PullRequestCard } from './PullRequestCard'; +import React from 'react'; + +export default { + title: 'Plugins/Azure Devops/Pull Request Card', + component: PullRequestCard, +}; + +const pullRequest: DashboardPullRequest = { + pullRequestId: 1, + title: + "feat(EXUX-4091): 🛂 Added the admin role authorization to the backend API's", + description: + 'This PR contains the following updates:\n\n| Package | Type | Update | Change |\n|---|---|---|---|\n| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint) | devDependencies | major | [`4.33.0` -> `5.0.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/4.33.0/5.0.0) |\n| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescrip', + link: undefined, + repository: { + id: undefined, + name: 'backstage', + url: undefined, + }, + createdBy: { + id: '', + displayName: 'Marley', + uniqueName: 'marley@test.com', + imageUrl: + 'https://dev.azure.com/exclaimerltd/_api/_common/identityImage?id=e6c0634b-68d2-6e6f-aa7d-adccada23216', + }, + reviewers: [ + { + id: undefined, + displayName: 'Marley', + imageUrl: '', + isRequired: true, + isContainer: false, + voteStatus: PullRequestVoteStatus.Approved, + }, + { + id: undefined, + displayName: 'User 1', + imageUrl: '', + isRequired: false, + isContainer: false, + voteStatus: PullRequestVoteStatus.WaitingForAuthor, + }, + { + id: undefined, + displayName: 'User 2', + imageUrl: '', + isRequired: true, + isContainer: false, + voteStatus: PullRequestVoteStatus.NoVote, + }, + ], + policies: [ + { + id: undefined, + type: PolicyType.Build, + status: PolicyEvaluationStatus.Running, + text: 'Build: UI (running)', + link: undefined, + }, + { + id: undefined, + type: PolicyType.MinimumReviewers, + text: 'Minimum number of reviewers (2)', + status: undefined, + link: undefined, + }, + { + id: undefined, + type: PolicyType.Comments, + text: 'Comment requirements', + status: undefined, + link: undefined, + }, + ], + hasAutoComplete: true, + creationDate: new Date(Date.now() - 10000000).toISOString(), + status: PullRequestStatus.Active, + isDraft: false, +}; + +export const Default = () => ( + + + +); + +export const Simplified = () => ( + + + +); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.tsx new file mode 100644 index 0000000000..b8b052425c --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.tsx @@ -0,0 +1,130 @@ +/* + * 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 { Avatar, Link } from '@backstage/core-components'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; + +import { AutoCompleteIcon } from '../AutoCompleteIcon'; +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { DateTime } from 'luxon'; +import { PullRequestCardPolicies } from './PullRequestCardPolicies'; +import { PullRequestCardReviewers } from './PullRequestCardReviewers'; +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles( + theme => ({ + card: { + backgroundColor: + theme.palette.type === 'dark' + ? theme.palette.grey[700] + : theme.palette.common.white, + }, + cardHeaderSimplified: { + paddingBottom: theme.spacing(2), + }, + cardHeaderAction: { + display: 'flex', + alignSelf: 'center', + margin: 0, + }, + content: { + display: 'flex', + flexDirection: 'row', + }, + policies: { + flex: 1, + }, + }), + { name: 'PullRequestCard' }, +); + +type PullRequestCardProps = { + pullRequest: DashboardPullRequest; + simplified?: boolean; +}; + +export const PullRequestCard = ({ + pullRequest, + simplified, +}: PullRequestCardProps) => { + const title = ( + + {pullRequest.title} + + ); + + const repoLink = ( + + {pullRequest.repository?.name} + + ); + + const creationDate = pullRequest.creationDate + ? DateTime.fromISO(pullRequest.creationDate).toRelative() + : undefined; + + const subheader = ( + + {repoLink}·{creationDate} + + ); + + const avatar = ( + + ); + + const classes = useStyles(); + + return ( + + + } + classes={{ + ...(simplified && { root: classes.cardHeaderSimplified }), + action: classes.cardHeaderAction, + }} + /> + + {!simplified && ( + + {pullRequest.policies && ( + + )} + + {pullRequest.reviewers && ( + + )} + + )} + + ); +}; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardPolicies.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardPolicies.tsx new file mode 100644 index 0000000000..ec6389ab1e --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardPolicies.tsx @@ -0,0 +1,35 @@ +/* + * 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 { Policy } from '@backstage/plugin-azure-devops-common'; +import { PullRequestCardPolicy } from './PullRequestCardPolicy'; +import React from 'react'; + +type PullRequestCardProps = { + policies: Policy[]; + className: string; +}; + +export const PullRequestCardPolicies = ({ + policies, + className, +}: PullRequestCardProps) => ( +
+ {policies.map(policy => ( + + ))} +
+); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardPolicy.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardPolicy.tsx new file mode 100644 index 0000000000..42343a8b27 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardPolicy.tsx @@ -0,0 +1,95 @@ +/* + * 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 { + Policy, + PolicyEvaluationStatus, + PolicyType, +} from '@backstage/plugin-azure-devops-common'; +import { styled, withStyles } from '@material-ui/core/styles'; + +import CancelIcon from '@material-ui/icons/Cancel'; +import GroupWorkIcon from '@material-ui/icons/GroupWork'; +import React from 'react'; +import WatchLaterIcon from '@material-ui/icons/WatchLater'; + +const PolicyRequiredIcon = withStyles( + theme => ({ + root: { + color: theme.palette.info.main, + }, + }), + { name: 'PolicyRequiredIcon' }, +)(WatchLaterIcon); + +const PolicyIssueIcon = withStyles( + theme => ({ + root: { + color: theme.palette.error.main, + }, + }), + { name: 'PolicyIssueIcon' }, +)(CancelIcon); + +const PolicyInProgressIcon = withStyles( + theme => ({ + root: { + color: theme.palette.info.main, + }, + }), + { name: 'PolicyInProgressIcon' }, +)(GroupWorkIcon); + +function getPolicyIcon(policy: Policy): JSX.Element | null { + switch (policy.type) { + case PolicyType.Build: + switch (policy.status) { + case PolicyEvaluationStatus.Running: + return ; + case PolicyEvaluationStatus.Rejected: + return ; + case PolicyEvaluationStatus.Queued: + return ; + default: + return null; + } + case PolicyType.MinimumReviewers: + return ; + case PolicyType.Status: + case PolicyType.Comments: + return ; + default: + return null; + } +} + +const PullRequestCardPolicyContainer = styled('div')({ + display: 'flex', + alignItems: 'center', + flexWrap: 'wrap', +}); + +type PullRequestCardPolicyProps = { + policy: Policy; +}; + +export const PullRequestCardPolicy = ({ + policy, +}: PullRequestCardPolicyProps) => ( + + {getPolicyIcon(policy)} {policy.text} + +); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardReviewer.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardReviewer.tsx new file mode 100644 index 0000000000..1d2049728b --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardReviewer.tsx @@ -0,0 +1,38 @@ +/* + * 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 { Avatar } from '@backstage/core-components'; +import React from 'react'; +import { Reviewer } from '@backstage/plugin-azure-devops-common'; + +type PullRequestCardReviewerProps = { + reviewer: Reviewer; +}; + +export const PullRequestCardReviewer = ({ + reviewer, +}: PullRequestCardReviewerProps) => ( + +); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardReviewers.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardReviewers.tsx new file mode 100644 index 0000000000..e3bf0c8a03 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCardReviewers.tsx @@ -0,0 +1,42 @@ +/* + * 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 { PullRequestCardReviewer } from './PullRequestCardReviewer'; +import React from 'react'; +import { Reviewer } from '@backstage/plugin-azure-devops-common'; +import { reviewerFilter } from '../utils'; +import { styled } from '@material-ui/core/styles'; + +const PullRequestCardReviewersContainer = styled('div')({ + '& > *': { + marginTop: 4, + marginBottom: 4, + }, +}); + +type PullRequestCardReviewersProps = { + reviewers: Reviewer[]; +}; + +export const PullRequestCardReviewers = ({ + reviewers, +}: PullRequestCardReviewersProps) => ( + + {reviewers.filter(reviewerFilter).map(reviewer => ( + + ))} + +); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/index.ts new file mode 100644 index 0000000000..5d6a49d09e --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { PullRequestCard } from './PullRequestCard'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/PullRequestGrid.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/PullRequestGrid.tsx new file mode 100644 index 0000000000..8b9c49beb4 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/PullRequestGrid.tsx @@ -0,0 +1,51 @@ +/* + * 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 { PullRequestGridColumn } from '../PullRequestGridColumn'; +import { PullRequestGroup } from '../types'; +import React from 'react'; +import { styled } from '@material-ui/core'; + +const GridDiv = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + '& > *': { + marginRight: theme.spacing(2), + }, + '& > :last-of-type': { + marginRight: 0, + }, +})); + +type PullRequestGridProps = { + pullRequestGroups: PullRequestGroup[]; +}; + +export const PullRequestGrid = ({ + pullRequestGroups, +}: PullRequestGridProps) => { + return ( + + {pullRequestGroups.map((pullRequestGroup, index) => ( + + ))} + + ); +}; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/index.ts new file mode 100644 index 0000000000..902680f80d --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { PullRequestGrid } from './PullRequestGrid'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/PullRequestGridColumn.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/PullRequestGridColumn.tsx new file mode 100644 index 0000000000..94a648188d --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/PullRequestGridColumn.tsx @@ -0,0 +1,87 @@ +/* + * 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 { Paper, Typography, styled, withStyles } from '@material-ui/core'; + +import { PullRequestCard } from '../PullRequestCard'; +import { PullRequestGroup } from '../types'; +import React from 'react'; + +const ColumnPaper = withStyles(theme => ({ + root: { + display: 'flex', + flexDirection: 'column', + flex: 1, + padding: theme.spacing(2), + backgroundColor: + theme.palette.type === 'dark' + ? theme.palette.grey[800] + : theme.palette.grey[300], + height: '100%', + }, +}))(Paper); + +const ColumnTitleDiv = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing(2), +})); + +export const PullRequestCardContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + '& > *': { + marginBottom: theme.spacing(2), + }, + '& > :last-of-type': { + marginBottom: 0, + }, +})); + +type PullRequestGridColumnProps = { + pullRequestGroup: PullRequestGroup; +}; + +export const PullRequestGridColumn = ({ + pullRequestGroup, +}: PullRequestGridColumnProps) => { + const columnTitle = ( + + {pullRequestGroup.title} + + ); + + const pullRequests = ( + + {pullRequestGroup.pullRequests.map(pullRequest => ( + + ))} + + ); + + return ( + + {columnTitle} + {pullRequests} + + ); +}; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/index.ts new file mode 100644 index 0000000000..aa3cf82554 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { PullRequestGridColumn } from './PullRequestGridColumn'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/types.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/types.ts new file mode 100644 index 0000000000..2d936f7b64 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/types.ts @@ -0,0 +1,36 @@ +/* + * 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 { + DashboardPullRequest, + Team, +} from '@backstage/plugin-azure-devops-common'; + +export interface PullRequestGroup { + title: string; + pullRequests: DashboardPullRequest[]; + simplified?: boolean; +} + +export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean; + +export type TeamFilter = (team: Team) => boolean; + +export interface PullRequestGroupConfig { + title: string; + filter: PullRequestFilter; + simplified?: boolean; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.test.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.test.ts new file mode 100644 index 0000000000..da27b2e06e --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { + DashboardPullRequest, + PullRequestVoteStatus, + Reviewer, +} from '@backstage/plugin-azure-devops-common'; +import { + arrayExtract, + getCreatedByUserFilter, + getPullRequestGroups, + reviewerFilter, +} from './utils'; + +describe('getCreatedByUserFilter', () => { + it('should filter if pull request is created by user', () => { + const userEmail = 'user1@backstage.com'; + const pr = { + createdBy: { uniqueName: userEmail }, + } as DashboardPullRequest; + const result = getCreatedByUserFilter(userEmail)(pr); + expect(result).toBe(true); + }); + + it('should not filter if pull request is not created by user', () => { + const userEmail1 = 'user1@backstage.com'; + const userEmail2 = 'user2@backstage.com'; + const pr = { + createdBy: { uniqueName: userEmail1 }, + } as DashboardPullRequest; + const result = getCreatedByUserFilter(userEmail2)(pr); + expect(result).toBe(false); + }); +}); + +describe('reviewerFilter', () => { + it('should return false if reviewer has no vote and is not required', () => { + const reviewer = { + voteStatus: PullRequestVoteStatus.NoVote, + isRequired: false, + } as Reviewer; + const result = reviewerFilter(reviewer); + expect(result).toBe(false); + }); + + it('should return true if reviewer has no vote and is required', () => { + const reviewer = { + voteStatus: PullRequestVoteStatus.NoVote, + isRequired: true, + } as Reviewer; + const result = reviewerFilter(reviewer); + expect(result).toBe(true); + }); + + it('should return true if reviewer has a vote and is not a container', () => { + const reviewer = { + voteStatus: PullRequestVoteStatus.Approved, + isContainer: false, + } as Reviewer; + const result = reviewerFilter(reviewer); + expect(result).toBe(true); + }); + + it('should return true if reviewer has a vote and is a container', () => { + const reviewer = { + voteStatus: PullRequestVoteStatus.Approved, + isContainer: true, + } as Reviewer; + const result = reviewerFilter(reviewer); + expect(result).toBe(false); + }); +}); + +describe('arrayExtract', () => { + it('should extract numbers greater than 3', () => { + const numbers = [1, 2, 3, 4, 5, 6]; + const numberFilter = (num: number): boolean => num > 3; + const extractedNumbers = arrayExtract(numbers, numberFilter); + expect(numbers).toEqual([1, 2, 3]); + expect(extractedNumbers).toEqual([4, 5, 6]); + }); + + it('should extract even numbers', () => { + const numbers = [1, 2, 3, 4, 5, 6]; + const numberFilter = (num: number): boolean => num % 2 === 0; + const extractedNumbers = arrayExtract(numbers, numberFilter); + expect(numbers).toEqual([1, 3, 5]); + expect(extractedNumbers).toEqual([2, 4, 6]); + }); +}); + +describe('getPullRequestGroups', () => { + it('should create groups of pull requests based on the provided configs', () => { + const userEmail = 'user1@backstage.com'; + const userEmail2 = 'user2@backstage.com'; + + const pullRequests = [ + { + pullRequestId: 1, + createdBy: { uniqueName: userEmail }, + } as DashboardPullRequest, + { + pullRequestId: 2, + createdBy: { uniqueName: userEmail }, + } as DashboardPullRequest, + { + pullRequestId: 3, + createdBy: { uniqueName: userEmail2 }, + } as DashboardPullRequest, + { + pullRequestId: 4, + createdBy: { uniqueName: userEmail2 }, + } as DashboardPullRequest, + ]; + + const configs = [ + { title: 'Created by me', filter: getCreatedByUserFilter(userEmail) }, + { title: 'Other PRs', filter: (_: unknown) => true, simplified: true }, + ]; + + const result = getPullRequestGroups(pullRequests, configs); + + expect(result.length).toBe(2); + + const group1 = result[0]; + expect(group1.title).toBe('Created by me'); + expect(group1.simplified).toBeFalsy(); + expect(group1.pullRequests.length).toBe(2); + expect(group1.pullRequests).toContainEqual( + expect.objectContaining({ pullRequestId: 1 }), + ); + expect(group1.pullRequests).toContainEqual( + expect.objectContaining({ pullRequestId: 2 }), + ); + + const group2 = result[1]; + expect(group2.title).toBe('Other PRs'); + expect(group2.simplified).toBe(true); + expect(group2.pullRequests.length).toBe(2); + expect(group2.pullRequests).toContainEqual( + expect.objectContaining({ pullRequestId: 3 }), + ); + expect(group2.pullRequests).toContainEqual( + expect.objectContaining({ pullRequestId: 4 }), + ); + }); +}); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.ts new file mode 100644 index 0000000000..c9cf866010 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.ts @@ -0,0 +1,117 @@ +/* + * 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 { + DashboardPullRequest, + PullRequestVoteStatus, + Reviewer, +} from '@backstage/plugin-azure-devops-common'; +import { + PullRequestFilter, + PullRequestGroup, + PullRequestGroupConfig, +} from './types'; + +/** + * Creates a filter that matches pull requests created by `userEmail`. + * @param userEmail an email to filter by. + * @returns a filter for pull requests created by `userEmail`. + */ +export function getCreatedByUserFilter( + userEmail: string | undefined, +): PullRequestFilter { + return (pullRequest: DashboardPullRequest): boolean => + pullRequest.createdBy?.uniqueName?.toLocaleLowerCase() === + userEmail?.toLocaleLowerCase(); +} + +/** + * Filters a reviewer based on vote status and if the reviewer is required. + * @param reviewer a reviewer to filter. + * @returns whether or not to filter the `reviewer`. + */ +export function reviewerFilter(reviewer: Reviewer): boolean { + return reviewer.voteStatus === PullRequestVoteStatus.NoVote + ? !!reviewer.isRequired + : !reviewer.isContainer; +} + +/** + * Removes values from the provided array and returns them. + * @param arr the array to extract values from. + * @param filter a filter used to extract values from the provided array. + * @returns the values that were extracted from the array. + * + * @example + * ```ts + * const numbers = [1, 2, 3, 4, 5, 6]; + * const numberFilter = (num: number): boolean => num > 3; + * const extractedNumbers = arrayExtract(numbers, numberFilter); + * console.log(numbers); // [1, 2, 3] + * console.log(extractedNumbers); // [4, 5, 6] + * ``` + * + * @example + * ```ts + * const numbers = [1, 2, 3, 4, 5, 6]; + * const numberFilter = (num: number): boolean => num % 2 === 0; + * const extractedNumbers = arrayExtract(numbers, numberFilter); + * console.log(numbers); // [1, 3, 5] + * console.log(extractedNumbers); // [2, 4, 6] + * ``` + */ +export function arrayExtract(arr: T[], filter: (value: T) => unknown): T[] { + const extractedValues: T[] = []; + + for (let i = 0; i - extractedValues.length < arr.length; i++) { + const offsetIndex = i - extractedValues.length; + + const value = arr[offsetIndex]; + + if (filter(value)) { + arr.splice(offsetIndex, 1); + extractedValues.push(value); + } + } + + return extractedValues; +} + +/** + * Creates groups of pull requests based on a list of `PullRequestGroupConfig`. + * @param pullRequests all pull requests to be split up into groups. + * @param configs the config used for splitting up the pull request groups. + * @returns a list of pull request groups. + */ +export function getPullRequestGroups( + pullRequests: DashboardPullRequest[], + configs: PullRequestGroupConfig[], +): PullRequestGroup[] { + const remainingPullRequests: DashboardPullRequest[] = [...pullRequests]; + const pullRequestGroups: PullRequestGroup[] = []; + + configs.forEach(({ title, filter: configFilter, simplified }) => { + const groupPullRequests = arrayExtract(remainingPullRequests, configFilter); + + pullRequestGroups.push({ + title, + pullRequests: groupPullRequests, + simplified, + }); + }); + + return pullRequestGroups; +} diff --git a/plugins/azure-devops/src/hooks/index.ts b/plugins/azure-devops/src/hooks/index.ts new file mode 100644 index 0000000000..44b94f3f4b --- /dev/null +++ b/plugins/azure-devops/src/hooks/index.ts @@ -0,0 +1,22 @@ +/* + * 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 * from './useAllTeams'; +export * from './useDashboardPullRequests'; +export * from './useProjectRepoFromEntity'; +export * from './usePullRequests'; +export * from './useRepoBuilds'; +export * from './useUserEmail'; diff --git a/plugins/azure-devops/src/hooks/useAllTeams.ts b/plugins/azure-devops/src/hooks/useAllTeams.ts new file mode 100644 index 0000000000..d32aa5e136 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useAllTeams.ts @@ -0,0 +1,42 @@ +/* + * 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 { Team } from '@backstage/plugin-azure-devops-common'; +import { azureDevOpsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; + +export function useAllTeams(): { + teams?: Team[]; + loading: boolean; + error?: Error; +} { + const api = useApi(azureDevOpsApiRef); + + const { + value: teams, + loading, + error, + } = useAsync(() => { + return api.getAllTeams(); + }, [api]); + + return { + teams, + loading, + error, + }; +} diff --git a/plugins/azure-devops/src/hooks/useDashboardPullRequests.ts b/plugins/azure-devops/src/hooks/useDashboardPullRequests.ts new file mode 100644 index 0000000000..977f3d4c0c --- /dev/null +++ b/plugins/azure-devops/src/hooks/useDashboardPullRequests.ts @@ -0,0 +1,42 @@ +/* + * 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 { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { azureDevOpsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; + +export function useDashboardPullRequests(project: string): { + pullRequests?: DashboardPullRequest[]; + loading: boolean; + error?: Error; +} { + const api = useApi(azureDevOpsApiRef); + + const { + value: pullRequests, + loading, + error, + } = useAsync(() => { + return api.getDashboardPullRequests(project); + }, [api, project]); + + return { + pullRequests, + loading, + error, + }; +} diff --git a/plugins/azure-devops/src/hooks/useUserEmail.ts b/plugins/azure-devops/src/hooks/useUserEmail.ts new file mode 100644 index 0000000000..4655815297 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useUserEmail.ts @@ -0,0 +1,22 @@ +/* + * 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 { identityApiRef, useApi } from '@backstage/core-plugin-api'; + +export function useUserEmail(): string | undefined { + const identityApi = useApi(identityApiRef); + return identityApi.getProfile().email; +} diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts index 4da533b534..c9b804bc8d 100644 --- a/plugins/azure-devops/src/index.ts +++ b/plugins/azure-devops/src/index.ts @@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { azureDevOpsPlugin, EntityAzurePipelinesContent, EntityAzurePullRequestsContent, isAzureDevOpsAvailable, + AzurePullRequestsPage, } from './plugin'; + +export { AzurePullRequestsIcon } from './components/AzurePullRequestsIcon'; diff --git a/plugins/azure-devops/src/plugin.ts b/plugins/azure-devops/src/plugin.ts index 7a5f80d0d3..003c7b7eee 100644 --- a/plugins/azure-devops/src/plugin.ts +++ b/plugins/azure-devops/src/plugin.ts @@ -16,6 +16,7 @@ import { azurePipelinesEntityContentRouteRef, + azurePullRequestDashboardRouteRef, azurePullRequestsEntityContentRouteRef, } from './routes'; import { @@ -46,6 +47,15 @@ export const azureDevOpsPlugin = createPlugin({ ], }); +export const AzurePullRequestsPage = azureDevOpsPlugin.provide( + createRoutableExtension({ + name: 'AzurePullRequestsPage', + component: () => + import('./components/PullRequestsPage').then(m => m.PullRequestsPage), + mountPoint: azurePullRequestDashboardRouteRef, + }), +); + export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide( createRoutableExtension({ name: 'EntityAzurePipelinesContent', diff --git a/plugins/azure-devops/src/routes.ts b/plugins/azure-devops/src/routes.ts index 7a4adea33e..881ed8cc30 100644 --- a/plugins/azure-devops/src/routes.ts +++ b/plugins/azure-devops/src/routes.ts @@ -16,6 +16,11 @@ import { createRouteRef } from '@backstage/core-plugin-api'; +export const azurePullRequestDashboardRouteRef = createRouteRef({ + id: 'azure-pull-request-dashboard', + path: '', +}); + export const azurePipelinesEntityContentRouteRef = createRouteRef({ id: 'azure-pipelines-entity-content', }); diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 5cd063d960..736d079969 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-badges-backend +## 0.1.12 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + ## 0.1.11 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 2186c8d52e..6f44e96b37 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,22 +31,21 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", + "@backstage/errors": "^0.1.5", "@types/express": "^4.17.6", "badge-maker": "^3.3.0", "cors": "^2.8.5", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 44bc597d0a..449352f87b 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-badges +## 0.2.15 + +### Patch Changes + +- 4149d74c10: Fix the path that the Badges client uses towards the `plugin-badges-backend` +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.2.14 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 4d02d64237..be38144ada 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.14", + "version": "0.2.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,9 +28,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/errors": "^0.1.3", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", + "@backstage/errors": "^0.1.5", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -42,10 +42,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts index 432a485a6b..8ad5d313e4 100644 --- a/plugins/badges/src/api/BadgesClient.ts +++ b/plugins/badges/src/api/BadgesClient.ts @@ -52,7 +52,7 @@ export class BadgesClient implements BadgesApi { private async getEntityBadgeSpecsUrl(entity: Entity): Promise { const routeParams = this.getEntityRouteParams(entity); - const path = generatePath(`:kind/:namespace/:name`, routeParams); + const path = generatePath(`:namespace/:kind/:name`, routeParams); return `${await this.discoveryApi.getBaseUrl( 'badges', )}/entity/${path}/badge-specs`; diff --git a/plugins/badges/src/components/EntityBadgesDialog.test.tsx b/plugins/badges/src/components/EntityBadgesDialog.test.tsx index 1f5ee94e04..ea44eb20fc 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.test.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.test.tsx @@ -16,12 +16,11 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { renderWithEffects } from '@backstage/test-utils'; +import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import { BadgesApi, badgesApiRef } from '../api'; import { EntityBadgesDialog } from './EntityBadgesDialog'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { ErrorApi, errorApiRef } from '@backstage/core-plugin-api'; describe('EntityBadgesDialog', () => { @@ -42,16 +41,16 @@ describe('EntityBadgesDialog', () => { const mockEntity = { metadata: { name: 'mock' } } as Entity; const rendered = await renderWithEffects( - - , + , ); await expect( diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 88e646b5e3..812ae8f5e3 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-bazaar-backend +## 0.1.3 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/backend-common@0.9.11 + ## 0.1.2 ### Patch Changes diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index a6610493e2..9c6426fcba 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/backend-test-utils": "^0.1.9", "@backstage/config": "^0.1.5", "@types/express": "^4.17.6", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.9.1" }, "files": [ "dist", diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index fd07a3b5c7..bb45a7b974 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -23,16 +23,16 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/cli": "^0.9.0", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog": "^0.7.3", "@backstage/plugin-catalog-react": "^0.6.4", + "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", - "@date-io/luxon": "1.x", "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", @@ -41,7 +41,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@backstage/dev-utils": "^0.2.13", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.0.6" diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 92cd9693e1..3af16837de 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -39,17 +39,16 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", - "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx index a206e28de6..82faa3e39c 100644 --- a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx +++ b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx @@ -21,14 +21,11 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers, renderInTestApp, + TestApiRegistry, } from '@backstage/test-utils'; import { useBitriseBuilds } from '../../hooks/useBitriseBuilds'; import { BitriseBuildsTable } from './BitriseBuildsTableComponent'; -import { - ApiProvider, - ApiRegistry, - UrlPatternDiscovery, -} from '@backstage/core-app-api'; +import { ApiProvider, UrlPatternDiscovery } from '@backstage/core-app-api'; jest.mock('../../hooks/useBitriseBuilds', () => ({ useBitriseBuilds: jest.fn(), @@ -40,10 +37,13 @@ describe('BitriseBuildsFetchComponent', () => { setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeEach(() => { - apis = ApiRegistry.with(bitriseApiRef, new BitriseClientApi(discoveryApi)); + apis = TestApiRegistry.from([ + bitriseApiRef, + new BitriseClientApi(discoveryApi), + ]); }); it('should display `no records` message if there are no builds', async () => { diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index a776f3f329..9e769af873 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/plugin-catalog-backend@0.18.0 + ## 0.3.6 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 5e3cec24d9..36cacb6660 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend modules that helps integrate towards LDAP", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/errors": "^0.1.4", - "@backstage/plugin-catalog-backend": "^0.17.4", + "@backstage/errors": "^0.1.5", + "@backstage/plugin-catalog-backend": "^0.18.0", "@backstage/types": "^0.1.1", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", @@ -40,7 +40,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index ee8aa49065..c2225b8945 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.18.0 + ## 0.2.9 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 1357659739..e1a837d0c1 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -13,6 +13,7 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import * as msal from '@azure/msal-node'; +import { Response as Response_2 } from 'node-fetch'; import { UserEntity } from '@backstage/catalog-model'; // Warning: (ae-missing-release-tag) "defaultGroupTransformer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -88,11 +89,11 @@ export class MicrosoftGraphClient { // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-catalog-backend-module-msgraph" does not have an export "ODataQuery" getUsers(query?: ODataQuery): AsyncIterable; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-catalog-backend-module-msgraph" does not have an export "ODataQuery" - requestApi(path: string, query?: ODataQuery): Promise; + requestApi(path: string, query?: ODataQuery): Promise; // Warning: (ae-forgotten-export) The symbol "ODataQuery" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-catalog-backend-module-msgraph" does not have an export "ODataQuery" requestCollection(path: string, query?: ODataQuery): AsyncIterable; - requestRaw(url: string): Promise; + requestRaw(url: string): Promise; } // Warning: (ae-missing-release-tag) "MicrosoftGraphOrgEntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 222f56b022..391a7125de 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend modules that helps integrate towards Microsoft Graph", - "version": "0.2.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,18 +32,19 @@ "@azure/msal-node": "^1.1.0", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/plugin-catalog-backend": "^0.17.4", + "@backstage/plugin-catalog-backend": "^0.18.0", "@microsoft/microsoft-graph-types": "^2.6.0", - "cross-fetch": "^3.0.6", + "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", + "node-fetch": "^2.6.1", "p-limit": "^3.0.2", "winston": "^3.2.1", "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.9.10", - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/backend-common": "^0.9.11", + "@backstage/cli": "^0.9.1", + "@backstage/test-utils": "^0.1.23", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 83b4371c0d..63f256afbd 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -16,7 +16,7 @@ import * as msal from '@azure/msal-node'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; -import fetch from 'cross-fetch'; +import fetch, { Response } from 'node-fetch'; import qs from 'qs'; import { MicrosoftGraphProviderConfig } from './config'; diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 92b57f3483..ce19314a50 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-catalog-backend +## 0.18.0 + +### Minor Changes + +- 7f82ce9f51: **BREAKING** EntitiesSearchFilter fields have changed. + + EntitiesSearchFilter now has only two fields: `key` and `value`. The `matchValueIn` and `matchValueExists` fields are no longer are supported. Previous filters written using the `matchValueIn` and `matchValueExists` fields can be rewritten as follows: + + Filtering by existence of key only: + + ```diff + filter: { + { + key: 'abc', + - matchValueExists: true, + }, + } + ``` + + Filtering by key and values: + + ```diff + filter: { + { + key: 'abc', + - matchValueExists: true, + - matchValueIn: ['xyz'], + + values: ['xyz'], + }, + } + ``` + + Negation of filters can now be achieved through a `not` object: + + ``` + filter: { + not: { + key: 'abc', + values: ['xyz'], + }, + } + ``` + +### Patch Changes + +- 740f958290: Providing an empty values array in an EntityFilter will now return no matches. +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- eddb82ab7c: Index User entities by displayName to be able to search by full name. Added displayName (if present) to the 'text' field in the indexed document. +- 563b039f0b: Added Azure DevOps discovery processor +- 8866b62f3d: Detect a duplicate entities when adding locations through dry run +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + ## 0.17.4 ### Patch Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 5008cf02f1..43eb8d61b7 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -31,6 +31,7 @@ import { ResourceEntityV1alpha1 } from '@backstage/catalog-model'; import { Router } from 'express'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; +import { TokenManager } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; import { Validators } from '@backstage/catalog-model'; @@ -174,6 +175,26 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor { ): Promise; } +// Warning: (ae-missing-release-tag) "AzureDevOpsDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { + constructor(options: { integrations: ScmIntegrations; logger: Logger_2 }); + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger_2; + }, + ): AzureDevOpsDiscoveryProcessor; + // (undocumented) + readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise; +} + // Warning: (ae-missing-release-tag) "BitbucketDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -736,8 +757,10 @@ export class DefaultCatalogCollator implements DocumentCollator { locationTemplate, filter, catalogClient, + tokenManager, }: { discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; locationTemplate?: string; filter?: CatalogEntitiesRequest['filter']; catalogClient?: CatalogApi; @@ -760,12 +783,15 @@ export class DefaultCatalogCollator implements DocumentCollator { _config: Config, options: { discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; filter?: CatalogEntitiesRequest['filter']; }, ): DefaultCatalogCollator; // (undocumented) protected locationTemplate: string; // (undocumented) + protected tokenManager: TokenManager; + // (undocumented) readonly type: string; } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index a9a35d5858..0628969ea3 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.17.4", + "version": "0.18.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/errors": "^0.1.4", + "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.9", "@backstage/search-common": "^0.2.1", "@backstage/types": "^0.1.1", @@ -43,7 +43,6 @@ "aws-sdk": "^2.840.0", "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "fast-json-stable-stringify": "^2.1.0", @@ -53,6 +52,7 @@ "knex": "^0.95.1", "lodash": "^4.17.21", "luxon": "^2.0.2", + "node-fetch": "^2.6.1", "p-limit": "^3.0.2", "prom-client": "^13.2.0", "uuid": "^8.0.0", @@ -63,8 +63,8 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.9", - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/test-utils": "^0.1.23", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts new file mode 100644 index 0000000000..ab053e8748 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -0,0 +1,277 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { LocationSpec } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { codeSearch } from './azure'; +import { + AzureDevOpsDiscoveryProcessor, + parseUrl, +} from './AzureDevOpsDiscoveryProcessor'; + +jest.mock('./azure'); +const mockCodeSearch = codeSearch as jest.MockedFunction; + +describe('AzureDevOpsDiscoveryProcessor', () => { + describe('parseUrl', () => { + it('parses well formed URLs', () => { + expect(parseUrl('https://dev.azure.com/my-org/my-proj')).toEqual({ + baseUrl: 'https://dev.azure.com', + org: 'my-org', + project: 'my-proj', + repo: '', + catalogPath: '/catalog-info.yaml', + }); + + expect( + parseUrl( + 'https://dev.azure.com/spotify/engineering/_git/backstage?path=/catalog.yaml', + ), + ).toEqual({ + baseUrl: 'https://dev.azure.com', + org: 'spotify', + project: 'engineering', + repo: 'backstage', + catalogPath: '/catalog.yaml', + }); + + expect( + parseUrl( + 'https://azuredevops.mycompany.com/spotify/engineering/_git/backstage?path=/src/*/catalog.yaml', + ), + ).toEqual({ + baseUrl: 'https://azuredevops.mycompany.com', + org: 'spotify', + project: 'engineering', + repo: 'backstage', + catalogPath: '/src/*/catalog.yaml', + }); + }); + + it('throws on incorrectly formed URLs', () => { + expect(() => parseUrl('https://dev.azure.com')).toThrow(); + expect(() => parseUrl('https://dev.azure.com//')).toThrow(); + expect(() => parseUrl('https://dev.azure.com//foo')).toThrow(); + }); + }); + + describe('reject unrelated entries', () => { + it('rejects unknown types', async () => { + const processor = AzureDevOpsDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + azure: [{ host: 'dev.azure.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'not-azure-discovery', + target: 'https://dev.azure.com', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).resolves.toBeFalsy(); + }); + }); + + it('rejects unknown targets', async () => { + const processor = AzureDevOpsDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [ + { host: 'dev.azure.com', token: 'blob' }, + { host: 'azure.myorg.com', token: 'blob' }, + ], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://not.azure.com/org/project', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).rejects.toThrow( + /There is no Azure integration that matches https:\/\/not.azure.com\/org\/project. Please add a configuration entry for it under integrations.azure/, + ); + }); + + describe('handles repositories', () => { + const processor = AzureDevOpsDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'dev.azure.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + + beforeEach(() => { + mockCodeSearch.mockClear(); + }); + + it('output all locations found on from code search', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://dev.azure.com/shopify/engineering', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + { + fileName: 'catalog-info.yaml', + path: '/src/catalog-info.yaml', + repository: { + name: 'ios-app', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ); + expect(emitter).toHaveBeenCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', + }, + optional: true, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/ios-app?path=/src/catalog-info.yaml', + }, + optional: true, + }); + }); + + it('output single locations from code search', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://dev.azure.com/shopify/engineering/_git/backstage', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ); + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', + }, + optional: true, + }); + }); + + it('output single locations with different file name from code search', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: + 'https://dev.azure.com/shopify/engineering?path=/src/*/catalog.yaml', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog.yaml', + path: '/src/main/catalog.yaml', + repository: { + name: 'backstage', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + '', + '/src/*/catalog.yaml', + ); + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/src/main/catalog.yaml', + }, + optional: true, + }); + }); + + it('output nothing when code search does not find anything', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://dev.azure.com/shopify/engineering/_git/backstage', + }; + mockCodeSearch.mockResolvedValueOnce([]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ); + expect(emitter).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts new file mode 100644 index 0000000000..58ee2155e9 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -0,0 +1,151 @@ +/* + * 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 { LocationSpec } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { Logger } from 'winston'; +import * as results from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; +import { codeSearch } from './azure'; + +/** + * Extracts repositories out of an Azure DevOps org. + * + * The following will create locations for all projects which have a catalog-info.yaml + * on the default branch. The first is shorthand for the second. + * + * target: "https://dev.azure.com/org/project" + * or + * target: https://dev.azure.com/org/project?path=/catalog-info.yaml + * + * You may also explicitly specify a single repo: + * + * target: https://dev.azure.com/org/project/_git/repo + **/ +export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { + private readonly integrations: ScmIntegrations; + private readonly logger: Logger; + + static fromConfig(config: Config, options: { logger: Logger }) { + const integrations = ScmIntegrations.fromConfig(config); + + return new AzureDevOpsDiscoveryProcessor({ + ...options, + integrations, + }); + } + + constructor(options: { integrations: ScmIntegrations; logger: Logger }) { + this.integrations = options.integrations; + this.logger = options.logger; + } + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise { + if (location.type !== 'azure-discovery') { + return false; + } + + const azureConfig = this.integrations.azure.byUrl(location.target)?.config; + if (!azureConfig) { + throw new Error( + `There is no Azure integration that matches ${location.target}. Please add a configuration entry for it under integrations.azure`, + ); + } + + const { baseUrl, org, project, repo, catalogPath } = parseUrl( + location.target, + ); + this.logger.info( + `Reading Azure DevOps repositories from ${location.target}`, + ); + + const files = await codeSearch( + azureConfig, + org, + project, + repo, + catalogPath, + ); + + this.logger.debug( + `Found ${files.length} files in Azure DevOps from ${location.target}.`, + ); + + for (const file of files) { + emit( + results.location( + { + type: 'url', + target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, + }, + // Not all locations may actually exist, since the user defined them as a wildcard pattern. + // Thus, we emit them as optional and let the downstream processor find them while not outputting + // an error if it couldn't. + true, + ), + ); + } + + return true; + } +} + +/** + * parseUrl extracts segments from the Azure DevOps URL. + **/ +export function parseUrl(urlString: string): { + baseUrl: string; + org: string; + project: string; + repo: string; + catalogPath: string; +} { + const url = new URL(urlString); + const path = url.pathname.substr(1).split('/'); + + const catalogPath = url.searchParams.get('path') || '/catalog-info.yaml'; + + if (path.length === 2 && path[0].length && path[1].length) { + return { + baseUrl: url.origin, + org: decodeURIComponent(path[0]), + project: decodeURIComponent(path[1]), + repo: '', + catalogPath, + }; + } else if ( + path.length === 4 && + path[0].length && + path[1].length && + path[2].length && + path[3].length + ) { + return { + baseUrl: url.origin, + org: decodeURIComponent(path[0]), + project: decodeURIComponent(path[1]), + repo: decodeURIComponent(path[3]), + catalogPath, + }; + } + + throw new Error(`Failed to parse ${urlString}`); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts b/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts new file mode 100644 index 0000000000..67cc120bab --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts @@ -0,0 +1,232 @@ +/* + * 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 { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { codeSearch, CodeSearchResponse } from './azure'; + +describe('azure', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + describe('codeSearch', () => { + it('returns empty when nothing is found', async () => { + const response: CodeSearchResponse = { count: 0, results: [] }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:*', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ), + ).resolves.toEqual([]); + }); + }); + + it('returns entries when request matches some files', async () => { + const response: CodeSearchResponse = { + count: 2, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'ios-app', + }, + }, + ], + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:*', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ), + ).resolves.toEqual(response.results); + }); + + it('searches in specific repo if parameter is set', async () => { + const response: CodeSearchResponse = { + count: 1, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + ], + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:backstage', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ), + ).resolves.toEqual(response.results); + }); + + it('can search using onpremise api', async () => { + const response: CodeSearchResponse = { + count: 1, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + ], + }; + + server.use( + rest.post( + `https://azuredevops.mycompany.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:*', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'azuredevops.mycompany.com', token: 'ABC' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ), + ).resolves.toEqual(response.results); + }); + + it('searches multiple pages if response contains many items', async () => { + const totalCount = 2401; + const generateItems = (count: number) => { + return Array.from(Array(count).keys()).map(_ => ({ + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + })); + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toMatchObject({ + searchText: 'path:/catalog-info.yaml repo:backstage', + $top: 1000, + }); + + const body = req.body as { $skip: number; $top: number }; + const countItemsToReturn = + body.$top + body.$skip > totalCount + ? totalCount - body.$skip + : body.$top; + + return res( + ctx.json({ + count: totalCount, + results: generateItems(countItemsToReturn), + }), + ); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ), + ).resolves.toHaveLength(totalCount); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts b/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts new file mode 100644 index 0000000000..316c9b0b8c --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts @@ -0,0 +1,80 @@ +/* + * 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 fetch from 'node-fetch'; +import { + AzureIntegrationConfig, + getAzureRequestOptions, +} from '@backstage/integration'; + +export interface CodeSearchResponse { + count: number; + results: CodeSearchResultItem[]; +} + +export interface CodeSearchResultItem { + fileName: string; + path: string; + repository: { + name: string; + }; +} + +const isCloud = (host: string) => host === 'dev.azure.com'; +const PAGE_SIZE = 1000; + +// codeSearch returns all files that matches the given search path. +export async function codeSearch( + azureConfig: AzureIntegrationConfig, + org: string, + project: string, + repo: string, + path: string, +): Promise { + const searchBaseUrl = isCloud(azureConfig.host) + ? 'https://almsearch.dev.azure.com' + : `https://${azureConfig.host}`; + const searchUrl = `${searchBaseUrl}/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; + + let items: CodeSearchResultItem[] = []; + let hasMorePages = true; + + do { + const response = await fetch(searchUrl, { + ...getAzureRequestOptions(azureConfig, { + 'Content-Type': 'application/json', + }), + method: 'POST', + body: JSON.stringify({ + searchText: `path:${path} repo:${repo || '*'}`, + $skip: items.length, + $top: PAGE_SIZE, + }), + }); + + if (response.status !== 200) { + throw new Error( + `Azure DevOps search failed with response status ${response.status}`, + ); + } + + const body: CodeSearchResponse = await response.json(); + items = [...items, ...body.results]; + hasMorePages = body.count > items.length; + } while (hasMorePages); + + return items; +} diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/index.ts b/plugins/catalog-backend/src/ingestion/processors/azure/index.ts new file mode 100644 index 0000000000..d4ff56c4c0 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/index.ts @@ -0,0 +1,17 @@ +/* + * 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 * from './azure'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts index b578799229..0c132bb61b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fetch from 'cross-fetch'; +import fetch from 'node-fetch'; import { BitbucketIntegrationConfig, diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts index 8781e071f7..d1765521a3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import fetch from 'cross-fetch'; +import fetch from 'node-fetch'; import { getGitLabRequestOptions, GitLabIntegrationConfig, diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index 4f3c502a8e..63feac51af 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -26,6 +26,7 @@ export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; +export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts index b8e45c7cf3..6fb52e245e 100644 --- a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts @@ -42,6 +42,7 @@ import { CodeOwnersProcessor, FileReaderProcessor, GithubDiscoveryProcessor, + AzureDevOpsDiscoveryProcessor, GithubOrgReaderProcessor, GitLabDiscoveryProcessor, LocationEntityProcessor, @@ -317,6 +318,7 @@ export class CatalogBuilder { new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, { logger }), GithubDiscoveryProcessor.fromConfig(config, { logger }), + AzureDevOpsDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), GitLabDiscoveryProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts index 8f1af6f615..1360ca2647 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultCatalogCollator } from './DefaultCatalogCollator'; import { setupServer } from 'msw/node'; @@ -55,6 +58,7 @@ const expectedEntities: Entity[] = [ describe('DefaultCatalogCollator', () => { let mockDiscoveryApi: jest.Mocked; + let mockTokenManager: jest.Mocked; let collator: DefaultCatalogCollator; beforeAll(() => { @@ -62,7 +66,14 @@ describe('DefaultCatalogCollator', () => { getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), getExternalBaseUrl: jest.fn(), }; - collator = new DefaultCatalogCollator({ discovery: mockDiscoveryApi }); + mockTokenManager = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; + collator = new DefaultCatalogCollator({ + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + }); server.listen(); }); beforeEach(() => { @@ -118,6 +129,7 @@ describe('DefaultCatalogCollator', () => { // Provide an alternate location template. collator = new DefaultCatalogCollator({ discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, locationTemplate: '/software/:name', }); @@ -131,6 +143,7 @@ describe('DefaultCatalogCollator', () => { // Provide an alternate location template. collator = DefaultCatalogCollator.fromConfig(new ConfigReader({}), { discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, filter: { kind: ['Foo', 'Bar'], }, diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 3e19b38ea6..4c6342c6c6 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { Entity, UserEntity } from '@backstage/catalog-model'; import { IndexableDocument, DocumentCollator } from '@backstage/search-common'; import { Config } from '@backstage/config'; @@ -38,11 +41,13 @@ export class DefaultCatalogCollator implements DocumentCollator { protected filter?: CatalogEntitiesRequest['filter']; protected readonly catalogClient: CatalogApi; public readonly type: string = 'software-catalog'; + protected tokenManager: TokenManager; static fromConfig( _config: Config, options: { discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; filter?: CatalogEntitiesRequest['filter']; }, ) { @@ -56,8 +61,10 @@ export class DefaultCatalogCollator implements DocumentCollator { locationTemplate, filter, catalogClient, + tokenManager, }: { discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; locationTemplate?: string; filter?: CatalogEntitiesRequest['filter']; catalogClient?: CatalogApi; @@ -68,6 +75,7 @@ export class DefaultCatalogCollator implements DocumentCollator { this.filter = filter; this.catalogClient = catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.tokenManager = tokenManager; } protected applyArgsToFormat( @@ -100,9 +108,13 @@ export class DefaultCatalogCollator implements DocumentCollator { } async execute() { - const response = await this.catalogClient.getEntities({ - filter: this.filter, - }); + const { token } = await this.tokenManager.getToken(); + const response = await this.catalogClient.getEntities( + { + filter: this.filter, + }, + { token }, + ); return response.items.map((entity: Entity): CatalogEntityDocument => { return { title: entity.metadata.title ?? entity.metadata.name, diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts index a089989d20..562bae4867 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts @@ -148,6 +148,57 @@ describe('DefaultLocationServiceTest', () => { expect(result.exists).toBe(true); }); + it('should fail when there are duplicate entities using dry run', async () => { + store.listLocations.mockResolvedValueOnce([]); + orchestrator.process.mockResolvedValueOnce({ + ok: true, + state: {}, + completedEntity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'foo', + }, + }, + deferredEntities: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'foo', + }, + }, + locationKey: 'file:///tmp/mock.yaml', + }, + ], + relations: [], + errors: [], + }); + + orchestrator.process.mockResolvedValueOnce({ + ok: true, + state: {}, + completedEntity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'foo', + }, + }, + deferredEntities: [], + relations: [], + errors: [], + }); + + await expect( + locationService.createLocation( + { type: 'url', target: 'https://backstage.io/catalog-info.yaml' }, + true, + ), + ).rejects.toThrowError('Duplicate nested entity: location:default/foo'); + }); + it('should return exists false when the location does not exist beforehand', async () => { orchestrator.process.mockResolvedValueOnce({ ok: true, diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index 2166341935..8dd2274842 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -19,6 +19,7 @@ import { LocationSpec, LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION, + stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogProcessingOrchestrator, @@ -54,6 +55,43 @@ export class DefaultLocationService implements LocationService { return this.store.deleteLocation(id); } + private async processEntities( + unprocessedEntities: DeferredEntity[], + ): Promise { + const entities: Entity[] = []; + while (unprocessedEntities.length) { + const currentEntity = unprocessedEntities.pop(); + if (!currentEntity) { + continue; + } + const processed = await this.orchestrator.process({ + entity: currentEntity.entity, + state: {}, // we process without the existing cache + }); + + if (processed.ok) { + if ( + entities.some( + e => + stringifyEntityRef(e) === + stringifyEntityRef(processed.completedEntity), + ) + ) { + throw new Error( + `Duplicate nested entity: ${stringifyEntityRef( + processed.completedEntity, + )}`, + ); + } + unprocessedEntities.push(...processed.deferredEntities); + entities.push(processed.completedEntity); + } else { + throw Error(processed.errors.map(String).join(', ')); + } + } + return entities; + } + private async dryRunCreateLocation( spec: LocationSpec, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> { @@ -86,24 +124,7 @@ export class DefaultLocationService implements LocationService { const unprocessedEntities: DeferredEntity[] = [ { entity, locationKey: `${spec.type}:${spec.target}` }, ]; - const entities: Entity[] = []; - while (unprocessedEntities.length) { - const currentEntity = unprocessedEntities.pop(); - if (!currentEntity) { - continue; - } - const processed = await this.orchestrator.process({ - entity: currentEntity.entity, - state: {}, // we process without the existing cache - }); - - if (processed.ok) { - unprocessedEntities.push(...processed.deferredEntities); - entities.push(processed.completedEntity); - } else { - throw Error(processed.errors.map(String).join(', ')); - } - } + const entities: Entity[] = await this.processEntities(unprocessedEntities); return { exists: await existsPromise, diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index eed420cd53..c9ebc8f723 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -44,6 +44,7 @@ import { CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, + AzureDevOpsDiscoveryProcessor, GithubDiscoveryProcessor, GithubOrgReaderProcessor, GitLabDiscoveryProcessor, @@ -292,6 +293,7 @@ export class NextCatalogBuilder { return [ new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, { logger }), + AzureDevOpsDiscoveryProcessor.fromConfig(config, { logger }), GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), GitLabDiscoveryProcessor.fromConfig(config, { logger }), diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts index ee86e78c6b..d0f284c11c 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts @@ -435,5 +435,37 @@ describe('NextEntitiesCatalog', () => { expect(entities).toContainEqual(entity1); }, ); + + it.each(databases.eachSupportedId())( + 'should return no matches for an empty values array', + // NOTE: An empty values array is not a sensible input in a realistic scenario. + async databaseId => { + const { knex } = await createDatabase(databaseId); + const entity1: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one' }, + spec: {}, + }; + const entity2: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two' }, + spec: {}, + }; + await addEntityToSearch(knex, entity1); + await addEntityToSearch(knex, entity2); + const catalog = new NextEntitiesCatalog(knex); + + const testFilter = { + key: 'kind', + values: [], + }; + const request = { filter: testFilter }; + const { entities } = await catalog.entities(request); + + expect(entities.length).toBe(0); + }, + ); }); }); diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts index 8c2de9d7c1..f9b3abf190 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts @@ -91,7 +91,7 @@ function addCondition( if (filter.values) { if (filter.values.length === 1) { this.where({ value: filter.values[0].toLowerCase() }); - } else if (filter.values.length > 1) { + } else { this.andWhere( 'value', 'in', diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 13869cfd38..16c942de8b 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -23,8 +23,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -41,10 +41,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 9fe482a30b..3c167946d4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -14,14 +14,19 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { CatalogApi, catalogApiRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { MockAnalyticsApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockAnalyticsApi, + renderInTestApp, + TestApiProvider, + TestApiRegistry, +} from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { catalogEntityRouteRef, catalogGraphRouteRef } from '../../routes'; @@ -31,7 +36,7 @@ describe('', () => { let entity: Entity; let wrapper: JSX.Element; let catalog: jest.Mocked; - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeAll(() => { Object.defineProperty(window.SVGElement.prototype, 'getBBox', { @@ -61,7 +66,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), }; - apis = ApiRegistry.with(catalogApiRef, catalog); + apis = TestApiRegistry.from([catalogApiRef, catalog]); wrapper = ( @@ -123,9 +128,9 @@ describe('', () => { test('captures analytics event on click', async () => { const analyticsSpy = new MockAnalyticsApi(); const { findByText } = await renderInTestApp( - + {wrapper} - , + , { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef, diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index b992d0c976..7bd1045fff 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -14,10 +14,13 @@ * limitations under the License. */ import { RELATION_HAS_PART, RELATION_PART_OF } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { MockAnalyticsApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockAnalyticsApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { catalogEntityRouteRef } from '../../routes'; @@ -90,10 +93,9 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), }; - const apis = ApiRegistry.with(catalogApiRef, catalog); wrapper = ( - + ', () => { selectedKinds: ['b'], }} /> - + ); }); @@ -172,9 +174,9 @@ describe('', () => { test('should capture analytics event when selecting other entity', async () => { const analyticsSpy = new MockAnalyticsApi(); const { getByText, findAllByTestId } = await renderInTestApp( - + {wrapper} - , + , { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef, @@ -195,9 +197,9 @@ describe('', () => { test('should capture analytics event when navigating to entity', async () => { const analyticsSpy = new MockAnalyticsApi(); const { getByText, findAllByTestId } = await renderInTestApp( - + {wrapper} - , + , { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index 2d6c5cc93d..be0c4dc5e5 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -21,9 +21,8 @@ import { RELATION_PART_OF, stringifyEntityRef, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React, { FunctionComponent } from 'react'; import { EntityRelationsGraph } from './EntityRelationsGraph'; @@ -158,10 +157,11 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), }; - const apis = ApiRegistry.with(catalogApiRef, catalog); Wrapper = ({ children }) => ( - {children} + + {children} + ); }); diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 933a84f13e..d46495a58b 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.14", @@ -55,10 +55,10 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx index 69ae38207d..784cbfbe9e 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx @@ -15,14 +15,10 @@ */ import { CatalogClient } from '@backstage/catalog-client'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { catalogImportApiRef, CatalogImportClient } from '../../api'; import { DefaultImportPage } from './DefaultImportPage'; @@ -43,15 +39,13 @@ describe('', () => { }, }; - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeEach(() => { - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ integrations: {} }), - ) - .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any })) - .with( + apis = TestApiRegistry.from( + [configApiRef, new ConfigReader({ integrations: {} })], + [catalogApiRef, new CatalogClient({ discoveryApi: {} as any })], + [ catalogImportApiRef, new CatalogImportClient({ discoveryApi: {} as any, @@ -63,7 +57,8 @@ describe('', () => { catalogApi: {} as any, configApi: {} as any, }), - ); + ], + ); }); it('renders without exploding', async () => { diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx index 4e0c3694cd..9a27532c57 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; -import { renderInTestApp } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + TestApiRegistry, +} from '@backstage/test-utils'; import React from 'react'; import { CatalogImportApi, catalogImportApiRef } from '../../api'; import { ImportInfoCard } from './ImportInfoCard'; describe('', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; let catalogImportApi: jest.Mocked; beforeEach(() => { @@ -35,26 +35,29 @@ describe('', () => { submitPullRequest: jest.fn(), }; - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ - integrations: { - github: [{ token: 'my-token' }], - }, - }), - ).with(catalogImportApiRef, catalogImportApi); + apis = TestApiRegistry.from( + [ + configApiRef, + new ConfigReader({ + integrations: { + github: [{ token: 'my-token' }], + }, + }), + ], + [catalogImportApiRef, catalogImportApi], + ); }); it('renders without exploding', async () => { - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ integrations: {} }), - ).with(catalogImportApiRef, catalogImportApi); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText('Register an existing component')).toBeInTheDocument(); diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx index 92a6077792..1778af73dc 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx @@ -15,14 +15,10 @@ */ import { CatalogClient } from '@backstage/catalog-client'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { useOutlet } from 'react-router'; import { catalogImportApiRef, CatalogImportClient } from '../../api'; @@ -49,15 +45,13 @@ describe('', () => { }, }; - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeEach(() => { - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ integrations: {} }), - ) - .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any })) - .with( + apis = TestApiRegistry.from( + [configApiRef, new ConfigReader({ integrations: {} })], + [catalogApiRef, new CatalogClient({ discoveryApi: {} as any })], + [ catalogImportApiRef, new CatalogImportClient({ discoveryApi: {} as any, @@ -67,7 +61,8 @@ describe('', () => { catalogApi: {} as any, configApi: new ConfigReader({}), }), - ); + ], + ); }); afterEach(() => jest.resetAllMocks()); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx index 4a72a653ea..8d28fb8604 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -34,14 +34,14 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} - + ); const location = { diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 0a55d6337b..3b6f353155 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { TestApiProvider } from '@backstage/test-utils'; import { TextField } from '@material-ui/core'; import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -54,13 +54,15 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} - + ); const onPrepareFn = jest.fn(); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 8eba11aeca..f6e9d296df 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -17,16 +17,15 @@ import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; import { capitalize } from 'lodash'; -import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { EntityTypePicker } from './EntityTypePicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { catalogApiRef } from '../../api'; import { EntityKindFilter, EntityTypeFilter } from '../../filters'; -import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { renderWithEffects } from '@backstage/test-utils'; +import { alertApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; const entities: Entity[] = [ { @@ -61,11 +60,20 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.with(catalogApiRef, { - getEntities: jest.fn().mockResolvedValue({ items: entities }), -} as unknown as CatalogApi).with(alertApiRef, { - post: jest.fn(), -} as unknown as AlertApi); +const apis = TestApiRegistry.from( + [ + catalogApiRef, + { + getEntities: jest.fn().mockResolvedValue({ items: entities }), + }, + ], + [ + alertApiRef, + { + post: jest.fn(), + }, + ], +); describe('', () => { it('renders available entity types', async () => { diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx index eccf824c2e..2b29bba4fd 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx @@ -24,7 +24,7 @@ import { CatalogClient } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { entityRouteRef } from '../../routes'; import { screen, waitFor } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import * as state from './useUnregisterEntityDialogState'; import { @@ -32,7 +32,6 @@ import { alertApiRef, DiscoveryApi, } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('UnregisterEntityDialog', () => { const discoveryApi: DiscoveryApi = { @@ -49,11 +48,6 @@ describe('UnregisterEntityDialog', () => { }, }; - const apis = ApiRegistry.with( - catalogApiRef, - new CatalogClient({ discoveryApi }), - ).with(alertApiRef, alertApi); - const entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -68,7 +62,14 @@ describe('UnregisterEntityDialog', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); const stateSpy = jest.spyOn(state, 'useUnregisterEntityDialogState'); diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx index 8b4f436aed..8d6083f111 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx @@ -31,7 +31,7 @@ import { UseUnregisterEntityDialogState, useUnregisterEntityDialogState, } from './useUnregisterEntityDialogState'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; function defer(): { promise: Promise; resolve: (value: T) => void } { let resolve: (value: T) => void = () => {}; @@ -51,9 +51,9 @@ describe('useUnregisterEntityDialogState', () => { const catalogApi = catalogApiMock as Partial as CatalogApi; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); let entity: Entity; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 4662e58dd2..f6a4c59374 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -26,9 +26,9 @@ import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityTagFilter, UserListFilter } from '../../filters'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { ConfigApi, configApiRef, @@ -62,12 +62,12 @@ const mockIdentityApi = { getIdToken: async () => undefined, } as Partial; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [configApiRef, mockConfigApi], [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], [storageApiRef, MockStorageApi.create()], -]); +); const mockIsOwnedEntity = (entity: Entity) => entity.metadata.name === 'component-1'; diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx index d801e508a6..cdcac8c194 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx @@ -15,12 +15,12 @@ */ import React, { PropsWithChildren } from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { catalogApiRef } from '../api'; import { renderHook } from '@testing-library/react-hooks'; import { useEntityKinds } from './useEntityKinds'; +import { TestApiProvider } from '@backstage/test-utils'; const entities: Entity[] = [ { @@ -59,9 +59,9 @@ const mockCatalogApi: Partial = { const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( - + {children} - + ); }; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index e448ee73f1..b96f63304a 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -16,7 +16,6 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { ConfigApi, configApiRef, @@ -24,7 +23,7 @@ import { identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; @@ -76,16 +75,6 @@ const mockCatalogApi: Partial = { getEntities: jest.fn().mockImplementation(async () => ({ items: entities })), getEntityByName: async () => undefined, }; -const apis = ApiRegistry.from([ - [configApiRef, mockConfigApi], - [catalogApiRef, mockCatalogApi], - [identityApiRef, mockIdentityApi], - [storageApiRef, MockStorageApi.create()], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ], -]); const wrapper = ({ userFilter, @@ -94,13 +83,26 @@ const wrapper = ({ userFilter?: UserListFilterKind; }>) => { return ( - + - + ); }; diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 4edad98c12..f01c13839d 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -21,8 +21,8 @@ import { RELATION_OWNED_BY, UserEntity, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { catalogApiRef } from '../api'; @@ -50,14 +50,14 @@ describe('useEntityOwnership', () => { const catalogApi = mockCatalogApi as unknown as CatalogApi; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} - + ); const ownedEntity: ComponentEntity = { diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index 4f41ef090f..aef876a1b2 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -15,9 +15,8 @@ */ import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; @@ -47,14 +46,16 @@ describe('useStarredEntities', () => { beforeEach(() => { mockStorage = MockStorageApi.create(); wrapper = ({ children }: PropsWithChildren<{}>) => ( - {children} - + ); }); diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx index b8577aec0c..8ffc891092 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx @@ -15,7 +15,7 @@ */ import { Entity, EntityName } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; import Observable from 'zen-observable'; @@ -31,11 +31,9 @@ describe('useStarredEntity', () => { beforeEach(() => { wrapper = ({ children }: PropsWithChildren<{}>) => ( - + {children} - + ); }); diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index ddee20c488..e0218090bd 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.3", "@backstage/integration-react": "^0.1.14", "@backstage/plugin-catalog-react": "^0.6.4", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index d6f13ed22d..afbed3bdc7 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -15,11 +15,7 @@ */ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrationsApi, scmIntegrationsApiRef, @@ -30,7 +26,7 @@ import { CatalogApi, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { viewTechDocRouteRef } from '../../routes'; @@ -71,21 +67,25 @@ describe('', () => { }, ], }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: {}, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -116,28 +116,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -167,28 +171,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByTitle } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -216,17 +224,21 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig(new ConfigReader({})), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -253,17 +265,21 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig(new ConfigReader({})), - ).with(catalogApiRef, catalogApi); const { getByTitle } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -295,17 +311,21 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig(new ConfigReader({})), - ).with(catalogApiRef, catalogApi); const { queryByTitle } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -332,28 +352,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/docs/:namespace/:kind/:name': viewTechDocRouteRef, @@ -381,28 +405,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -429,28 +457,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index 2730665fb6..b320d6d220 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -18,13 +18,12 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; import { - CatalogApi, catalogApiRef, EntityKindFilter, MockEntityListContextProvider, } from '@backstage/plugin-catalog-react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { renderWithEffects } from '@backstage/test-utils'; +import { ApiProvider } from '@backstage/core-app-api'; +import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; import { CatalogKindHeader } from './CatalogKindHeader'; const entities: Entity[] = [ @@ -58,9 +57,12 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.with(catalogApiRef, { - getEntities: jest.fn().mockResolvedValue({ items: entities }), -} as Partial); +const apis = TestApiRegistry.from([ + catalogApiRef, + { + getEntities: jest.fn().mockResolvedValue({ items: entities }), + }, +]); describe('', () => { it('renders available kinds', async () => { diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 6b33b33b08..0a46580c12 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -20,7 +20,6 @@ import { RELATION_MEMBER_OF, RELATION_OWNED_BY, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { IdentityApi, @@ -38,6 +37,7 @@ import { mockBreakpoint, MockStorageApi, renderWithEffects, + TestApiProvider, wrapInTestApp, } from '@backstage/test-utils'; import DashboardIcon from '@material-ui/icons/Dashboard'; @@ -129,8 +129,8 @@ describe('CatalogPage', () => { const renderWrapped = (children: React.ReactNode) => renderWithEffects( wrapInTestApp( - { starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi }), ], - ])} + ]} > {children} - , + , { mountedRoutes: { '/create': createComponentRouteRef, diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 822913a385..4c6adcf400 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -19,7 +19,7 @@ import { Entity, VIEW_URL_ANNOTATION, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { entityRouteRef, DefaultStarredEntitiesApi, @@ -27,7 +27,11 @@ import { starredEntitiesApiRef, UserListFilter, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import * as React from 'react'; import { CatalogTable } from './CatalogTable'; @@ -51,10 +55,10 @@ const entities: Entity[] = [ ]; describe('CatalogTable component', () => { - const mockApis = ApiRegistry.with( + const mockApis = TestApiRegistry.from([ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ); + ]); beforeEach(() => { window.open = jest.fn(); diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx index 7dd823a8f0..e43104b67d 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { DependencyOfComponentsCard } from './DependencyOfComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx index b9fd75def9..b654136d18 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { DependsOnComponentsCard } from './DependsOnComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx index bee759254c..40685705aa 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { DependsOnResourcesCard } from './DependsOnResourcesCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 2216070984..3f3fc804e1 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -16,7 +16,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { AsyncEntityProvider, @@ -26,7 +26,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; @@ -40,12 +44,14 @@ const mockEntity = { }, } as Entity; -const mockApis = ApiRegistry.with(catalogApiRef, {} as CatalogApi) - .with(alertApiRef, {} as AlertApi) - .with( +const mockApis = TestApiRegistry.from( + [catalogApiRef, {} as CatalogApi], + [alertApiRef, {} as AlertApi], + [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ); + ], +); describe('EntityLayout', () => { it('renders simplest case', async () => { diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx index 862ef9504b..f58e6b9b66 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx @@ -20,10 +20,9 @@ import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { screen, waitFor } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('DeleteEntityDialog', () => { const alertApi: jest.Mocked = { @@ -34,10 +33,6 @@ describe('DeleteEntityDialog', () => { const catalogClient: jest.Mocked = { removeEntityByUid: jest.fn(), } as any; - const apis = ApiRegistry.with(catalogApiRef, catalogClient).with( - alertApiRef, - alertApi, - ); const entity = { apiVersion: 'backstage.io/v1alpha1', @@ -54,7 +49,14 @@ describe('DeleteEntityDialog', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); afterEach(() => { diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx index 9e26184a8d..a8aac405fe 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx @@ -15,23 +15,16 @@ */ import { - CatalogApi, catalogApiRef, catalogRouteRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { EntityOrphanWarning } from './EntityOrphanWarning'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogClient: jest.Mocked = { - removeEntityByUid: jest.fn(), - } as any; - const apis = ApiRegistry.with(catalogApiRef, catalogClient); - it('renders EntityOrphanWarning if the entity is orphan', async () => { const entity = { apiVersion: 'v1', @@ -50,11 +43,20 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx index c406d0ed3c..fcc4475f6a 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -21,17 +21,17 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { EntityProcessingErrorsPanel } from './EntityProcessingErrorsPanel'; import { Entity, getEntityName } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; describe('', () => { - const catalogClient: jest.Mocked = { - getEntityAncestors: jest.fn(), - } as any; - const apis = ApiRegistry.with(catalogApiRef, catalogClient); + const getEntityAncestors: jest.MockedFunction< + CatalogApi['getEntityAncestors'] + > = jest.fn(); + const apis = TestApiRegistry.from([catalogApiRef, { getEntityAncestors }]); it('renders EntityProcessErrors if the entity has errors', async () => { const entity: Entity = { @@ -97,7 +97,7 @@ describe('', () => { }, }; - catalogClient.getEntityAncestors.mockResolvedValue({ + getEntityAncestors.mockResolvedValue({ root: getEntityName(entity), items: [{ entity, parents: [] }], }); @@ -198,7 +198,7 @@ describe('', () => { ], }, }; - catalogClient.getEntityAncestors.mockResolvedValue({ + getEntityAncestors.mockResolvedValue({ root: getEntityName(entity), items: [ { entity, parents: [getEntityName(parent)] }, diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index 7f8bf35b74..8768e9728e 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -21,17 +21,14 @@ import React from 'react'; import { isKind } from './conditions'; import { EntitySwitch } from './EntitySwitch'; import { featureFlagsApiRef } from '@backstage/core-plugin-api'; -import { - LocalStorageFeatureFlags, - ApiProvider, - ApiRegistry, -} from '@backstage/core-app-api'; +import { LocalStorageFeatureFlags } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; const mockFeatureFlagsApi = new LocalStorageFeatureFlags(); const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); describe('EntitySwitch', () => { diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx index d484ab026a..45f7561d6e 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasComponentsCard } from './HasComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx index 45604ceb56..a01922d988 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasResourcesCard } from './HasResourcesCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -92,7 +84,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx index 431e757656..e517bd38ae 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasSubcomponentsCard } from './HasSubcomponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx index 3fcfed6099..050550101f 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasSystemsCard } from './HasSystemsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -95,7 +87,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx index 2b35d0f3b6..e0ec48fbed 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx @@ -21,10 +21,9 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { Entity, RELATION_PART_OF } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { SystemDiagramCard } from './SystemDiagramCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { beforeAll(() => { @@ -55,11 +54,11 @@ describe('', () => { }; const { queryByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -114,11 +113,11 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -173,11 +172,11 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 7228044f4f..6f3eaa39e3 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -52,10 +52,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 4125a8c82e..f66dcbed00 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 8e8f9ceac8..f1175172f5 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-code-coverage-backend +## 0.1.15 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + ## 0.1.14 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 90320d0e41..1bc637146b 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.1.14", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,14 +20,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", + "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.8", "@types/express": "^4.17.6", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-xml-bodyparser": "^0.3.0", @@ -37,7 +36,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index e5b51e90b4..3a45960684 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -23,8 +23,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.4", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", @@ -42,10 +42,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index ca889ebdbd..fc4f8697b8 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-config-schema +## 0.1.14 + +### Patch Changes + +- 9f21236a29: Fixed a missing `await` when throwing server side errors +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.1.13 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index c9abbc7e4e..0d0357cd7c 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.13", + "version": "0.1.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,9 +22,9 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/errors": "^0.1.4", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", + "@backstage/errors": "^0.1.5", "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", @@ -37,10 +37,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 7096af37c6..c43d7c0612 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cost-insights +## 0.11.12 + +### Patch Changes + +- 950b36393c: Supply featureFlags using featureFlag config option. +- Updated dependencies + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.11.11 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index da9ba8b1f6..12133fcc64 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.11.11", + "version": "0.11.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,10 +55,10 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx index 4a38af3dbc..e29bb78f2f 100644 --- a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx @@ -15,10 +15,10 @@ */ import { CostInsightsHeader } from './CostInsightsHeader'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; describe('', () => { @@ -29,7 +29,7 @@ describe('', () => { }), }; - const apis = ApiRegistry.from([[identityApiRef, identityApi]]); + const apis = TestApiRegistry.from([identityApiRef, identityApi]); it('Shows nothing to do when no alerts exist', async () => { const rendered = await renderInTestApp( diff --git a/plugins/cost-insights/src/plugin.ts b/plugins/cost-insights/src/plugin.ts index 1714c5fbd5..a422098e08 100644 --- a/plugins/cost-insights/src/plugin.ts +++ b/plugins/cost-insights/src/plugin.ts @@ -34,9 +34,7 @@ export const unlabeledDataflowAlertRef = createRouteRef({ export const costInsightsPlugin = createPlugin({ id: 'cost-insights', - register({ featureFlags }) { - featureFlags.register('cost-insights-currencies'); - }, + featureFlags: [{ name: 'cost-insights-currencies' }], routes: { root: rootRouteRef, growthAlerts: projectGrowthAlertRef, diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 75450c0bb7..09d2feb71b 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -30,8 +30,9 @@ import { Group, Duration } from '../types'; // TODO(Rugvip): Could be good to have a clear place to put test utils that is linted accordingly // eslint-disable-next-line import/no-extraneous-dependencies -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { TestApiProvider } from '@backstage/test-utils'; type PartialPropsWithChildren = PropsWithChildren>; @@ -199,16 +200,17 @@ export const MockCostInsightsApiProvider = ({ getUserGroups: jest.fn(), }; - // TODO: defaultConfigApiRef: ConfigApiRef - - const defaultContext = ApiRegistry.from([ - [identityApiRef, { ...defaultIdentityApi, ...context.identityApi }], - [ - costInsightsApiRef, - { ...defaultCostInsightsApi, ...context.costInsightsApi }, - ], - // [configApiRef, { ...defaultConfigApiRef, ...context.configApiRef }] - ]); - - return {children}; + return ( + + {children} + + ); }; diff --git a/plugins/explore/package.json b/plugins/explore/package.json index d9945edc51..8988a7f215 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/plugin-explore-react": "^0.0.7", "@backstage/theme": "^0.2.13", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index 8927cf088b..d10c262357 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -15,11 +15,10 @@ */ import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor, getByText } from '@testing-library/react'; import React from 'react'; import { DefaultExplorePage } from './DefaultExplorePage'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -36,9 +35,9 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); beforeEach(() => { diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index d78fc0428d..46a8f1adf2 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -16,12 +16,11 @@ import { DomainEntity } from '@backstage/catalog-model'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { catalogEntityRouteRef } from '../../routes'; import { DomainExplorerContent } from './DomainExplorerContent'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -38,9 +37,9 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); const mountedRoutes = { diff --git a/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx index 80944628da..7574c685a4 100644 --- a/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx +++ b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx @@ -14,16 +14,12 @@ * limitations under the License. */ -import { - ApiProvider, - ApiRegistry, - FeatureFlagged, -} from '@backstage/core-app-api'; +import { FeatureFlagged } from '@backstage/core-app-api'; import { FeatureFlagsApi, featureFlagsApiRef, } from '@backstage/core-plugin-api'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ExploreLayout } from './ExploreLayout'; @@ -35,11 +31,11 @@ const featureFlagsApi: jest.Mocked = { registerFlag: jest.fn(), }; -const mockApis = ApiRegistry.with(featureFlagsApiRef, featureFlagsApi); - describe('', () => { const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); afterEach(() => { diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx index 66f54900c9..7c57de84bc 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx @@ -20,10 +20,9 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { GroupsDiagram } from './GroupsDiagram'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { beforeAll(() => { @@ -57,9 +56,9 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index f0815e0116..5d3f7c358b 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -37,9 +36,9 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); const mountedRoutes = { diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx index 8accf8fd30..da35c9fa39 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx @@ -18,13 +18,12 @@ import { ExploreTool, exploreToolsConfigRef, } from '@backstage/plugin-explore-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ToolExplorerContent } from './ToolExplorerContent'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const exploreToolsConfigApi: jest.Mocked = { @@ -33,11 +32,9 @@ describe('', () => { const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index f332efc150..421ec10c1c 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx index 1123f014da..20dbb77e86 100644 --- a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ import React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { fireHydrantApiRef } from '../../api'; import { screen } from '@testing-library/react'; import { ServiceDetailsCard } from './ServiceDetailsCard'; import { Service, Incident } from '../types'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; const mockFireHydrantApi = { - getServiceDetails: () => {}, - getServiceAnalytics: () => {}, + getServiceDetails: jest.fn(), + getServiceAnalytics: jest.fn(), }; -const apis = ApiRegistry.from([[fireHydrantApiRef, mockFireHydrantApi]]); +const apis = TestApiRegistry.from([fireHydrantApiRef, mockFireHydrantApi]); jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index cc0b486d60..3731d01661 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", @@ -49,16 +49,15 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx index 79b401e4a8..046e3e9860 100644 --- a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx +++ b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { FossaApi, fossaApiRef } from '../../api'; import { FossaCard } from './FossaCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const fossaApi: jest.Mocked = { @@ -30,10 +29,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(fossaApiRef, fossaApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index 778f5ee9c6..421ff4431f 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -20,11 +20,10 @@ import { catalogApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { FossaApi, fossaApiRef } from '../../api'; import { FossaPage } from './FossaPage'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -46,13 +45,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(fossaApiRef, fossaApi).with( - catalogApiRef, - catalogApi, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index cd1c3ac4a1..3068119de5 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 7f7ff6184d..fd1ef75ccc 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/integration": "^0.6.8", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -39,10 +39,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 5eed309d68..7992f201cf 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-github-actions +## 0.4.25 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.4.24 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index ae7f32be1f..ea5f104c96 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.4.24", + "version": "0.4.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/integration": "^0.6.8", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", @@ -52,10 +52,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 4a4452be5c..0f9573e051 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -24,16 +24,13 @@ import { useWorkflowRuns } from '../useWorkflowRuns'; import type { Props as RecentWorkflowRunsCardProps } from './RecentWorkflowRunsCard'; import { RecentWorkflowRunsCard } from './RecentWorkflowRunsCard'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; import { errorApiRef, configApiRef, ConfigApi, } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; jest.mock('../useWorkflowRuns', () => ({ useWorkflowRuns: jest.fn(), @@ -82,16 +79,16 @@ describe('', () => { render( - - + , ); diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 062113a8ef..18c3cc3d05 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.8", "@backstage/integration-react": "^0.1.14", @@ -39,10 +39,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 08eeb4a3ce..dba5bbfe1c 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -19,6 +19,7 @@ import { fireEvent } from '@testing-library/react'; import { setupRequestMockHandlers, renderInTestApp, + TestApiRegistry, } from '@backstage/test-utils'; import { GithubDeployment, @@ -42,11 +43,7 @@ import { Entity } from '@backstage/catalog-model'; import { GithubDeploymentsTable } from './GithubDeploymentsTable'; import { Box } from '@material-ui/core'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { errorApiRef, configApiRef, @@ -95,14 +92,14 @@ const githubAuthApi: OAuthApi = { getAccessToken: async _ => 'access_token', }; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [configApiRef, configApi], [errorApiRef, errorApiMock], [ githubDeploymentsApiRef, new GithubDeploymentsApiClient({ scmIntegrationsApi, githubAuthApi }), ], -]); +); const assertFetchedData = async () => { const rendered = await renderInTestApp( diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index dfbd6297cf..8456eb4919 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx index 8dba498507..e158bcf6ad 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import React from 'react'; @@ -23,7 +23,6 @@ import ProfileCatalog from './ProfileCatalog'; import { ApiProvider, - ApiRegistry, GithubAuth, OAuthRequestManager, UrlPatternDiscovery, @@ -34,7 +33,7 @@ import { githubAuthApiRef } from '@backstage/core-plugin-api'; describe('ProfileCatalog', () => { it('should render', async () => { const oauthRequestApi = new OAuthRequestManager(); - const apis = ApiRegistry.from([ + const apis = TestApiRegistry.from( [gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')], [ githubAuthApiRef, @@ -45,7 +44,7 @@ describe('ProfileCatalog', () => { oauthRequestApi, }), ], - ]); + ); const { getByText } = await renderInTestApp( diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index f6c8b9fde8..4fbf6b0dab 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-graphiql +## 0.2.22 + +### Patch Changes + +- cd398cd4ab: Letting GraphiQL use headers +- Updated dependencies + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.2.21 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 5c72a4c728..a04f4d6992 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.21", + "version": "0.2.22", "private": false, "publishConfig": { "access": "public", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,10 +44,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx index 4e426d4364..ae84b549ec 100644 --- a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx +++ b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx @@ -19,14 +19,10 @@ import { GraphiQLPage } from './GraphiQLPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { act } from 'react-dom/test-utils'; -import { renderWithEffects } from '@backstage/test-utils'; +import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import { GraphQLBrowseApi, graphQlBrowseApiRef } from '../../lib/api'; import { configApiRef } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; jest.mock('../GraphiQLBrowser', () => ({ GraphiQLBrowser: () => '', @@ -43,17 +39,17 @@ describe('GraphiQLPage', () => { }; const rendered = await renderWithEffects( - , - , + , ); act(() => { jest.advanceTimersByTime(250); @@ -71,16 +67,16 @@ describe('GraphiQLPage', () => { }; const rendered = await renderWithEffects( - - , + , ); rendered.getByText('GraphiQL'); @@ -95,16 +91,16 @@ describe('GraphiQLPage', () => { }; const rendered = await renderWithEffects( - - , + , ); rendered.getByText('GraphiQL'); diff --git a/plugins/home/package.json b/plugins/home/package.json index ef13ce3a00..65f6fe2ee3 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 6011dd4f46..4dddf17cd9 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", @@ -39,10 +39,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 48f4461f7f..5cc91e0d5d 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-jenkins-backend +## 0.1.8 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/backend-common@0.9.11 + ## 0.1.7 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index ac9efb3ffd..4db26d94c4 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.7", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "jenkins": "^0.28.1", @@ -35,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 369676b195..1daba68a1d 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-jenkins +## 0.5.13 + +### Patch Changes + +- ad433b346f: Fix Jenkins project table pagination. +- Updated dependencies + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.5.12 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 4456b15284..18341f6eec 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.5.12", + "version": "0.5.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx index 39187fe859..b1eaf2c24c 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -205,6 +205,10 @@ export const CITableView = ({ onChangePageSize, total, }: Props) => { + const projectsInPage = projects?.slice( + page * pageSize, + Math.min(projects.length, (page + 1) * pageSize), + ); return ( retry(), }, ]} - data={projects ?? []} + data={projectsInPage ?? []} onPageChange={onChangePage} onRowsPerPageChange={onChangePageSize} title={ diff --git a/plugins/jenkins/src/components/Cards/Cards.test.tsx b/plugins/jenkins/src/components/Cards/Cards.test.tsx index 2508461710..0af91da179 100644 --- a/plugins/jenkins/src/components/Cards/Cards.test.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.test.tsx @@ -15,11 +15,10 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { LatestRunCard } from './Cards'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { JenkinsApi, jenkinsApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { Project } from '../../api/JenkinsApi'; describe('', () => { @@ -41,14 +40,12 @@ describe('', () => { }; it('should show success status of latest build', async () => { - const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApi]]); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText('Completed')).toBeInTheDocument(); @@ -59,14 +56,12 @@ describe('', () => { getProjects: () => Promise.reject(new Error('Unauthorized')), }; - const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText("Error: Can't connect to Jenkins")).toBeInTheDocument(); @@ -82,14 +77,12 @@ describe('', () => { }), }; - const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText("Error: Can't find Jenkins project")).toBeInTheDocument(); diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 482c1f75b1..9a417b04b0 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kafka-backend +## 0.2.12 + +### Patch Changes + +- 4f81bfd356: Add ACL requirements for kafka-backend plugin +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + ## 0.2.11 ### Patch Changes diff --git a/plugins/kafka-backend/README.md b/plugins/kafka-backend/README.md index d18a232fa4..637a5773e0 100644 --- a/plugins/kafka-backend/README.md +++ b/plugins/kafka-backend/README.md @@ -49,3 +49,7 @@ kafka: username: my-username password: my-password ``` + +### ACLs + +If you are using ACLs on Kafka, you will need to have the `DESCRIBE` ACL on both consumer groups and topics. diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index dea803890f..a8f64b21e1 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.11", + "version": "0.2.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", + "@backstage/errors": "^0.1.5", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -44,7 +44,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index a6fbe36e99..8da053ab50 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -35,10 +35,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx index a90d3d608e..0be9cabacd 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx @@ -26,8 +26,8 @@ import { import { useConsumerGroupsOffsetsForEntity } from './useConsumerGroupsOffsetsForEntity'; import * as data from './__fixtures__/consumer-group-offsets.json'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; const consumerGroupOffsets = data as ConsumerGroupOffsetsResponse; @@ -59,14 +59,14 @@ describe('useConsumerGroupOffsets', () => { const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( - {children} - + ); }; diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index d506571a3f..102c9c13e4 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes-backend +## 0.3.19 + +### Patch Changes + +- 37dc844728: Include CronJobs and Jobs as default objects returned by the kubernetes backend and add/update relevant types. +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/plugin-kubernetes-common@0.1.6 + - @backstage/backend-common@0.9.11 + ## 0.3.18 ### Patch Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index f34815f328..e1236e61c5 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -202,6 +202,8 @@ export type KubernetesObjectTypes = | 'deployments' | 'replicasets' | 'horizontalpodautoscalers' + | 'jobs' + | 'cronjobs' | 'ingresses' | 'customresources'; diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md index 5f541bb61d..d4725e38c0 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/README.md +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -27,14 +27,15 @@ Add or update `app-config.local.yaml` with the following: ```yaml kubernetes: - serviceLocatorMethod: 'multiTenant' + serviceLocatorMethod: + type: 'multiTenant' clusterLocatorMethods: - - 'config' - clusters: - - url: - name: minikube - serviceAccountToken: - authProvider: 'serviceAccount' + - type: 'config' + clusters: + - url: + name: minikube + serviceAccountToken: + authProvider: 'serviceAccount' ``` ### Getting the service account token diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml index 68ef489170..fb71037129 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml +++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml @@ -200,6 +200,35 @@ spec: maxReplicas: 15 targetCPUUtilizationPercentage: 50 +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: dice-roller-cronjob + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + schedule: '*/1 * * * *' + jobTemplate: + metadata: + labels: + 'backstage.io/kubernetes-id': dice-roller + spec: + template: + metadata: + labels: + 'backstage.io/kubernetes-id': dice-roller + spec: + containers: + - name: busybox + image: busybox + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Rolling a die! + restartPolicy: OnFailure + --- apiVersion: networking.k8s.io/v1beta1 kind: Ingress diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 4edc7b238a..0b3f7cb3cb 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.3.18", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/errors": "^0.1.3", - "@backstage/plugin-kubernetes-common": "^0.1.4", + "@backstage/errors": "^0.1.5", + "@backstage/plugin-kubernetes-common": "^0.1.6", "@google-cloud/container": "^2.2.0", "@kubernetes/client-node": "^0.15.0", "@types/express": "^4.17.6", @@ -55,7 +55,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/aws4": "^1.5.1", "supertest": "^6.1.3", "aws-sdk-mock": "^5.2.1", diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts index 8b96d5ceca..7f5183e337 100644 --- a/plugins/kubernetes-backend/schema.d.ts +++ b/plugins/kubernetes-backend/schema.d.ts @@ -23,6 +23,8 @@ export interface Config { | 'deployments' | 'replicasets' | 'horizontalpodautoscalers' + | 'jobs' + | 'cronjobs' | 'ingresses' >; serviceLocatorMethod: { @@ -61,5 +63,23 @@ export interface Config { apiVersion: string; plural: string; }>; + + /** + * (Optional) API Version Overrides + * If set, the specified api version will be used to make requests for the corresponding object. + * If running a legacy Kubernetes version, you may use this to override the default api versions + * that are not supported in your cluster. + */ + apiVersionOverrides?: { + pods?: string; + services?: string; + configmaps?: string; + deployments?: string; + replicasets?: string; + horizontalpodautoscalers?: string; + cronjobs?: string; + jobs?: string; + ingresses?: string; + } & { [pluralKind: string]: string }; }; } diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 11cca4917f..3333f7a0fc 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -243,6 +243,10 @@ export class KubernetesBuilder { 'kubernetes.objectTypes', ) as KubernetesObjectTypes[]; + const apiVersionOverrides = this.env.config.getOptionalConfig( + 'kubernetes.apiVersionOverrides', + ); + let objectTypesToFetch; if (objectTypesToFetchStrings) { @@ -250,6 +254,17 @@ export class KubernetesBuilder { objectTypesToFetchStrings.includes(obj.objectType), ); } + + if (apiVersionOverrides) { + objectTypesToFetch = objectTypesToFetch ?? DEFAULT_OBJECTS; + + for (const obj of objectTypesToFetch) { + if (apiVersionOverrides.has(obj.objectType)) { + obj.apiVersion = apiVersionOverrides.getString(obj.objectType); + } + } + } + return objectTypesToFetch; } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts index 4b2848508e..4a91118c7d 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts @@ -16,6 +16,7 @@ import { AppsV1Api, + BatchV1beta1Api, AutoscalingV1Api, CoreV1Api, KubeConfig, @@ -74,6 +75,12 @@ export class KubernetesClientProvider { return kc.makeApiClient(AutoscalingV1Api); } + getBatchClientByClusterDetails(clusterDetails: ClusterDetails) { + const kc = this.getKubeConfig(clusterDetails); + + return kc.makeApiClient(BatchV1beta1Api); + } + getNetworkingBeta1Client(clusterDetails: ClusterDetails) { const kc = this.getKubeConfig(clusterDetails); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts index 1f856e90a9..41d75a9835 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -68,6 +68,18 @@ export const DEFAULT_OBJECTS: ObjectToFetch[] = [ plural: 'horizontalpodautoscalers', objectType: 'horizontalpodautoscalers', }, + { + group: 'batch', + apiVersion: 'v1', + plural: 'jobs', + objectType: 'jobs', + }, + { + group: 'batch', + apiVersion: 'v1', + plural: 'cronjobs', + objectType: 'cronjobs', + }, { group: 'networking.k8s.io', apiVersion: 'v1', diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts index 308b7a10be..685df85c33 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -17,6 +17,7 @@ import { AppsV1Api, AutoscalingV1Api, + BatchV1beta1Api, CoreV1Api, NetworkingV1beta1Api, } from '@kubernetes/client-node'; @@ -41,6 +42,7 @@ export interface Clients { core: CoreV1Api; apps: AppsV1Api; autoscaling: AutoscalingV1Api; + batch: BatchV1beta1Api; networkingBeta1: NetworkingV1beta1Api; } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index 90485fcaee..b8aa482e47 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -67,6 +67,8 @@ export type KubernetesObjectTypes = | 'deployments' | 'replicasets' | 'horizontalpodautoscalers' + | 'jobs' + | 'cronjobs' | 'ingresses' | 'customresources'; diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 0f685bd4d4..b0e7aca129 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-kubernetes-common +## 0.1.6 + +### Patch Changes + +- 37dc844728: Include CronJobs and Jobs as default objects returned by the kubernetes backend and add/update relevant types. + ## 0.1.5 ### Patch Changes diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md index 9f9669275f..7db05a7513 100644 --- a/plugins/kubernetes-common/api-report.md +++ b/plugins/kubernetes-common/api-report.md @@ -6,8 +6,10 @@ import { Entity } from '@backstage/catalog-model'; import { ExtensionsV1beta1Ingress } from '@kubernetes/client-node'; import { V1ConfigMap } from '@kubernetes/client-node'; +import { V1CronJob } from '@kubernetes/client-node'; import { V1Deployment } from '@kubernetes/client-node'; import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node'; +import { V1Job } from '@kubernetes/client-node'; import { V1Pod } from '@kubernetes/client-node'; import { V1ReplicaSet } from '@kubernetes/client-node'; import { V1Service } from '@kubernetes/client-node'; @@ -48,6 +50,16 @@ export interface ConfigMapFetchResponse { type: 'configmaps'; } +// Warning: (ae-missing-release-tag) "CronJobsFetchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface CronJobsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'cronjobs'; +} + // Warning: (ae-missing-release-tag) "CustomResourceFetchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -78,6 +90,8 @@ export type FetchResponse = | DeploymentFetchResponse | ReplicaSetsFetchResponse | HorizontalPodAutoscalersFetchResponse + | JobsFetchResponse + | CronJobsFetchResponse | IngressesFetchResponse | CustomResourceFetchResponse; @@ -101,6 +115,16 @@ export interface IngressesFetchResponse { type: 'ingresses'; } +// Warning: (ae-missing-release-tag) "JobsFetchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface JobsFetchResponse { + // (undocumented) + resources: Array; + // (undocumented) + type: 'jobs'; +} + // Warning: (ae-missing-release-tag) "KubernetesErrorTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 3c9cb5e357..a3ab7710c0 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.1.5", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,7 @@ "@kubernetes/client-node": "^0.15.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0" + "@backstage/cli": "^0.9.1" }, "jest": { "roots": [ diff --git a/plugins/kubernetes-common/src/types.ts b/plugins/kubernetes-common/src/types.ts index add661a5f8..8f4daa111b 100644 --- a/plugins/kubernetes-common/src/types.ts +++ b/plugins/kubernetes-common/src/types.ts @@ -17,8 +17,10 @@ import { ExtensionsV1beta1Ingress, V1ConfigMap, + V1CronJob, V1Deployment, V1HorizontalPodAutoscaler, + V1Job, V1Pod, V1ReplicaSet, V1Service, @@ -83,6 +85,8 @@ export type FetchResponse = | DeploymentFetchResponse | ReplicaSetsFetchResponse | HorizontalPodAutoscalersFetchResponse + | JobsFetchResponse + | CronJobsFetchResponse | IngressesFetchResponse | CustomResourceFetchResponse; @@ -116,6 +120,16 @@ export interface HorizontalPodAutoscalersFetchResponse { resources: Array; } +export interface JobsFetchResponse { + type: 'jobs'; + resources: Array; +} + +export interface CronJobsFetchResponse { + type: 'cronjobs'; + resources: Array; +} + export interface IngressesFetchResponse { type: 'ingresses'; resources: Array; diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 0ae3a6c012..437ea327e4 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes +## 0.4.21 + +### Patch Changes + +- 3739d3f773: Implement support for formatting OpenShift dashboard url links +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.1.6 + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.4.20 ### Patch Changes diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index a2c1114c2d..c7a99e7334 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -29,7 +29,9 @@ import { } from '@backstage/plugin-kubernetes-common'; import fixture1 from '../src/__fixtures__/1-deployments.json'; import fixture2 from '../src/__fixtures__/2-deployments.json'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import fixture3 from '../src/__fixtures__/1-cronjobs.json'; +import fixture4 from '../src/__fixtures__/2-cronjobs.json'; +import { TestApiProvider } from '@backstage/test-utils'; const mockEntity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -80,32 +82,52 @@ createDevApp() path: '/fixture-1', title: 'Fixture 1', element: ( - - + ), }) .addPage({ path: '/fixture-2', title: 'Fixture 2', element: ( - - + + ), + }) + .addPage({ + path: '/fixture-3', + title: 'Fixture 3', + element: ( + + + + + + ), + }) + .addPage({ + path: '/fixture-4', + title: 'Fixture 4', + element: ( + + + + + ), }) .registerPlugin(kubernetesPlugin) diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 618f814c21..a127c9edf6 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.4.20", + "version": "0.4.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,15 +33,16 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", - "@backstage/plugin-kubernetes-common": "^0.1.5", + "@backstage/plugin-kubernetes-common": "^0.1.6", "@backstage/theme": "^0.2.13", "@kubernetes/client-node": "^0.15.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "cronstrue": "^1.122.0", "js-yaml": "^4.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", @@ -51,10 +52,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kubernetes/src/__fixtures__/1-cronjobs.json b/plugins/kubernetes/src/__fixtures__/1-cronjobs.json new file mode 100644 index 0000000000..1edc43710d --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/1-cronjobs.json @@ -0,0 +1,446 @@ +{ + "cronJobs": [ + { + "metadata": { + "name": "dice-roller-cronjob", + "namespace": "default", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "schedule": "30 5 * * *", + "startingDeadlineSeconds": 1800, + "concurrencyPolicy": "Forbid", + "suspend": false, + "jobTemplate": { + "metadata": { "creationTimestamp": null }, + "spec": { + "backoffLimit": 2, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "name": "busybox", + "image": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent", + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"] + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + } + }, + "successfulJobsHistoryLimit": 2, + "failedJobsHistoryLimit": 2 + }, + "status": { + "active": [ + { + "kind": "Job", + "namespace": "default", + "name": "dice-roller-cronjob-1637028600", + "uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "apiVersion": "batch/v1", + "resourceVersion": "1361174163" + } + ], + "lastScheduleTime": "2021-11-16T02:10:00Z" + } + } + ], + "jobs": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000", + "namespace": "default", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "resourceVersion": "1361029181", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "CronJob", + "name": "dice-roller-cronjob", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "parallelism": 1, + "completions": 1, + "backoffLimit": 2, + "selector": { + "matchLabels": { + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + }, + "status": { + "conditions": [ + { + "type": "Complete", + "status": "True", + "lastProbeTime": "2021-11-16T01:11:31Z", + "lastTransitionTime": "2021-11-16T01:11:31Z" + } + ], + "startTime": "2021-11-16T01:10:24Z", + "completionTime": "2021-11-16T01:11:31Z", + "succeeded": 1 + } + }, + { + "metadata": { + "name": "dice-roller-cronjob-1637028600", + "namespace": "default", + "uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "resourceVersion": "1361174166", + "creationTimestamp": "2021-11-16T02:10:22Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "CronJob", + "name": "dice-roller-cronjob", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "parallelism": 1, + "completions": 1, + "backoffLimit": 2, + "selector": { + "matchLabels": { + "controller-uid": "32be1b89-5b53-45b2-aa84-277e75214f61" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "job-name": "dice-roller-cronjob-1637028600" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + }, + "status": { "startTime": "2021-11-16T02:10:22Z", "active": 1 } + } + ], + "pods": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000-gstc4", + "generateName": "dice-roller-cronjob-1637025000-", + "namespace": "default", + "uid": "3b0f2b65-5ae2-441a-beda-bdc92bcafaf0", + "resourceVersion": "1361029179", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637025000", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Succeeded", + "conditions": [ + { + "type": "Initialized", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:10:27Z", + "reason": "PodCompleted" + }, + { + "type": "Ready", + "status": "False", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:11:31Z", + "reason": "PodCompleted" + }, + { + "type": "ContainersReady", + "status": "False", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:11:31Z", + "reason": "PodCompleted" + }, + { + "type": "PodScheduled", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T01:10:24Z" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-16T01:10:24Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "terminated": { + "exitCode": 0, + "reason": "Completed", + "startedAt": "2021-11-16T01:10:31Z", + "finishedAt": "2021-11-16T01:11:30Z", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615" + } + }, + "lastState": {}, + "ready": false, + "restartCount": 0, + "image": "busybox:latest/node", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615", + "started": false + } + ], + "qosClass": "Burstable" + } + }, + { + "metadata": { + "name": "dice-roller-cronjob-1637028600-p4mlc", + "generateName": "dice-roller-cronjob-1637028600-", + "namespace": "default", + "uid": "acddd5d2-ac7f-473b-a9d8-17a89f99ac39", + "resourceVersion": "1361174579", + "creationTimestamp": "2021-11-16T02:10:22Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "job-name": "dice-roller-cronjob-1637028600" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637028600", + "uid": "32be1b89-5b53-45b2-aa84-277e75214f61", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Running", + "conditions": [ + { + "type": "Initialized", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:25Z" + }, + { + "type": "Ready", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:35Z" + }, + { + "type": "ContainersReady", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:35Z" + }, + { + "type": "PodScheduled", + "status": "True", + "lastProbeTime": null, + "lastTransitionTime": "2021-11-16T02:10:22Z" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-16T02:10:22Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "running": { "startedAt": "2021-11-16T02:10:31Z" } + }, + "lastState": {}, + "ready": true, + "restartCount": 0, + "image": "busybox:latest", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615", + "started": true + } + ], + "qosClass": "Burstable" + } + } + ] +} diff --git a/plugins/kubernetes/src/__fixtures__/2-cronjobs.json b/plugins/kubernetes/src/__fixtures__/2-cronjobs.json new file mode 100644 index 0000000000..6c087023b7 --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/2-cronjobs.json @@ -0,0 +1,385 @@ +{ + "cronJobs": [ + { + "metadata": { + "name": "dice-roller-cronjob", + "namespace": "default", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "schedule": "* */2 * * *", + "startingDeadlineSeconds": 1800, + "concurrencyPolicy": "Forbid", + "suspend": true, + "jobTemplate": { + "metadata": { "creationTimestamp": null }, + "spec": { + "backoffLimit": 2, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "name": "busybox", + "image": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent", + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"] + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + } + }, + "successfulJobsHistoryLimit": 2, + "failedJobsHistoryLimit": 2 + }, + "status": { + "lastScheduleTime": "2021-11-16T02:10:00Z" + } + } + ], + "jobs": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000", + "namespace": "default", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "resourceVersion": "1361029181", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "CronJob", + "name": "dice-roller-cronjob", + "uid": "9ea073bc-7a4b-4b99-8321-0305bce85568", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "parallelism": 1, + "completions": 1, + "backoffLimit": 2, + "selector": { + "matchLabels": { + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {} + }, + "spec": { + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + } + }, + "status": { + "conditions": [ + { + "type": "Failed", + "status": "True", + "reason": "BackoffLimitExceeded", + "lastProbeTime": "2021-11-16T01:11:31Z", + "lastTransitionTime": "2021-11-16T01:11:31Z" + } + ], + "startTime": "2021-11-16T01:10:24Z", + "failed": 2 + } + } + ], + "pods": [ + { + "metadata": { + "name": "dice-roller-cronjob-1637025000-gstc4", + "generateName": "dice-roller-cronjob-1637025000-", + "namespace": "default", + "uid": "3b0f2b65-5ae2-441a-beda-bdc92bcafaf0", + "resourceVersion": "1361029179", + "creationTimestamp": "2021-11-16T01:10:24Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637025000", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Failed", + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:13Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:08Z", + "status": "True", + "type": "PodScheduled" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-18T19:10:08Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "terminated": { + "exitCode": 1, + "finishedAt": "2021-11-18T19:11:01Z", + "reason": "Error", + "startedAt": "2021-11-18T19:10:17Z", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615" + } + }, + "lastState": {}, + "ready": false, + "restartCount": 0, + "image": "busybox:latest", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://1062343e1bb3625e36717bc15617c1bbca976366c3c9dd87035c71f33d015615", + "started": false + } + ], + "qosClass": "Burstable" + } + }, + { + "metadata": { + "name": "dice-roller-cronjob-1637025000-p4mlc", + "generateName": "dice-roller-cronjob-1637025000-", + "namespace": "default", + "uid": "acddd5d2-ac7f-473b-a9d8-17a89f99ac39", + "resourceVersion": "1361174579", + "creationTimestamp": "2021-11-16T02:10:22Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "controller-uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "job-name": "dice-roller-cronjob-1637025000" + }, + "annotations": {}, + "ownerReferences": [ + { + "apiVersion": "batch/v1", + "kind": "Job", + "name": "dice-roller-cronjob-1637025000", + "uid": "69d5d242-a9a4-47b8-b9c7-c536ae8f151a", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "volumes": [], + "containers": [ + { + "command": ["/bin/sh", "-c", "date; echo Rolling a die!"], + "image": "busybox", + "imagePullPolicy": "IfNotPresent", + "name": "busybox", + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "restartPolicy": "Never", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "minikube", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [ + { + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, + { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + } + ], + "priority": 0, + "enableServiceLinks": true + }, + "status": { + "phase": "Failed", + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:13Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:11:02Z", + "message": "containers with unready status: [busybox]", + "reason": "ContainersNotReady", + "status": "False", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2021-11-18T19:10:08Z", + "status": "True", + "type": "PodScheduled" + } + ], + "hostIP": "192.168.49.2", + "podIP": "172.17.0.25", + "podIPs": [{ "ip": "172.17.0.25" }], + "startTime": "2021-11-16T02:10:22Z", + "containerStatuses": [ + { + "name": "busybox", + "state": { + "terminated": { + "exitCode": 1, + "finishedAt": "2021-11-18T19:11:01Z", + "reason": "Error", + "startedAt": "2021-11-18T19:10:17Z", + "containerID": "docker://2659c4d0f8a68f2b49863c18738322f1686d5b87275428e5e641fd9fd9e06739" + } + }, + "lastState": {}, + "ready": true, + "restartCount": 0, + "image": "busybox:latest", + "imageID": "docker-pullable://busybox@sha256:e7157b6d7ebbe2cce5eaa8cfe8aa4fa82d173999b9f90a9ec42e57323546c353", + "containerID": "docker://2659c4d0f8a68f2b49863c18738322f1686d5b87275428e5e641fd9fd9e06739", + "started": true + } + ], + "qosClass": "Burstable" + } + } + ] +} diff --git a/plugins/kubernetes/src/components/Cluster/Cluster.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.tsx index d238396f53..416d6e23e3 100644 --- a/plugins/kubernetes/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes/src/components/Cluster/Cluster.tsx @@ -29,6 +29,7 @@ import { DeploymentsAccordions } from '../DeploymentsAccordions'; import { groupResponses } from '../../utils/response'; import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; +import { CronJobsAccordions } from '../CronJobsAccordions'; import { CustomResources } from '../CustomResources'; import { ClusterContext, @@ -133,6 +134,9 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { + + + diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx new file mode 100644 index 0000000000..2434631fd0 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 from 'react'; +import { render } from '@testing-library/react'; +import { CronJobsAccordions } from './CronJobsAccordions'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import * as twoCronJobsFixture from '../../__fixtures__/2-cronjobs.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('CronJobsAccordions', () => { + it('should render 1 active cronjobs', async () => { + const wrapper = kubernetesProviders(oneCronJobsFixture, []); + + const { getByText } = render( + wrapper(wrapInTestApp()), + ); + + expect(getByText('dice-roller-cronjob')).toBeInTheDocument(); + expect(getByText('CronJob')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + expect(getByText('Active')).toBeInTheDocument(); + }); + + it('should render 1 suspended cronjobs', async () => { + const wrapper = kubernetesProviders(twoCronJobsFixture, []); + + const { getByText } = render( + wrapper(wrapInTestApp()), + ); + + expect(getByText('dice-roller-cronjob')).toBeInTheDocument(); + expect(getByText('CronJob')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + expect(getByText('Suspended')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx new file mode 100644 index 0000000000..2581678928 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsAccordions.tsx @@ -0,0 +1,128 @@ +/* + * 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, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1CronJob, V1Job } from '@kubernetes/client-node'; +import { JobsAccordions } from '../JobsAccordions'; +import { CronJobDrawer } from './CronJobsDrawer'; +import { getOwnedResources } from '../../utils/owner'; +import { GroupedResponsesContext } from '../../hooks'; +import { StatusError, StatusOK } from '@backstage/core-components'; +import cronstrue from 'cronstrue'; + +type CronJobsAccordionsProps = { + children?: React.ReactNode; +}; + +type CronJobAccordionProps = { + cronJob: V1CronJob; + ownedJobs: V1Job[]; + children?: React.ReactNode; +}; + +type CronJobSummaryProps = { + cronJob: V1CronJob; + children?: React.ReactNode; +}; + +const CronJobSummary = ({ cronJob }: CronJobSummaryProps) => { + return ( + + + + + + + + + + {cronJob.spec?.suspend ? ( + Suspended + ) : ( + Active + )} + + + + Schedule:{' '} + {cronJob.spec?.schedule + ? `${cronJob.spec.schedule} (${cronstrue.toString( + cronJob.spec.schedule, + )})` + : 'N/A'} + + + + + ); +}; + +const CronJobAccordion = ({ cronJob, ownedJobs }: CronJobAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +export const CronJobsAccordions = ({}: CronJobsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {groupedResponses.cronJobs.map((cronJob, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.test.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.test.tsx new file mode 100644 index 0000000000..b16e83ee50 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 from 'react'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { CronJobDrawer } from './CronJobsDrawer'; + +describe('CronJobDrawer', () => { + it('should render cronJob drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + , + ); + + expect(getAllByText('dice-roller-cronjob')).toHaveLength(2); + expect(getAllByText('CronJob')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Schedule')).toBeInTheDocument(); + expect(getByText('30 5 * * *')).toBeInTheDocument(); + expect(getByText('Starting Deadline Seconds')).toBeInTheDocument(); + expect(getByText('Last Schedule Time')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx new file mode 100644 index 0000000000..758f17ade0 --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/CronJobsDrawer.tsx @@ -0,0 +1,67 @@ +/* + * 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 from 'react'; +import { V1CronJob } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { Typography, Grid, Chip } from '@material-ui/core'; + +export const CronJobDrawer = ({ + cronJob, + expanded, +}: { + cronJob: V1CronJob; + expanded?: boolean; +}) => { + const namespace = cronJob.metadata?.namespace; + return ( + ({ + schedule: cronJobObj.spec?.schedule ?? '???', + startingDeadlineSeconds: + cronJobObj.spec?.startingDeadlineSeconds ?? '???', + concurrencyPolicy: cronJobObj.spec?.concurrencyPolicy ?? '???', + lastScheduleTime: cronJobObj.status?.lastScheduleTime ?? '???', + })} + > + + + + {cronJob.metadata?.name ?? 'unknown object'} + + + + + CronJob + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes/src/components/CronJobsAccordions/index.ts b/plugins/kubernetes/src/components/CronJobsAccordions/index.ts new file mode 100644 index 0000000000..e72e3f7e6d --- /dev/null +++ b/plugins/kubernetes/src/components/CronJobsAccordions/index.ts @@ -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 { CronJobsAccordions } from './CronJobsAccordions'; diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx index e27c2bfbe1..2fa140b5f7 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentsAccordions.tsx @@ -36,11 +36,12 @@ import { getOwnedPodsThroughReplicaSets, getMatchingHpa, } from '../../utils/owner'; +import { containersReady, totalRestarts } from '../../utils/pod'; import { GroupedResponsesContext, PodNamesWithErrorsContext, } from '../../hooks'; -import { StatusError, StatusOK } from '@backstage/core-components'; +import { StatusError, StatusOK, TableColumn } from '@backstage/core-components'; type DeploymentsAccordionsProps = { children?: React.ReactNode; @@ -61,6 +62,20 @@ type DeploymentSummaryProps = { children?: React.ReactNode; }; +const deploymentPodColumns: TableColumn[] = [ + { + title: 'containers ready', + align: 'center', + render: containersReady, + }, + { + title: 'total restarts', + align: 'center', + render: totalRestarts, + type: 'numeric', + }, +]; + const DeploymentSummary = ({ deployment, numberOfCurrentPods, @@ -161,7 +176,7 @@ const DeploymentAccordion = ({ /> - + ); diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx new file mode 100644 index 0000000000..093d616319 --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 from 'react'; +import { render } from '@testing-library/react'; +import { JobsAccordions } from './JobsAccordions'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; +import { V1Job, ObjectSerializer } from '@kubernetes/client-node'; + +describe('JobsAccordions', () => { + it('should render 2 jobs', async () => { + const wrapper = kubernetesProviders(oneCronJobsFixture, []); + + const jobs: V1Job[] = oneCronJobsFixture.jobs.map( + job => ObjectSerializer.deserialize(job, 'V1Job') as V1Job, + ); + + const { getByText } = render( + wrapper(wrapInTestApp()), + ); + + expect(getByText('dice-roller-cronjob-1637028600')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + + expect(getByText('dice-roller-cronjob-1637025000')).toBeInTheDocument(); + expect(getByText('Succeeded')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx new file mode 100644 index 0000000000..715be4c88a --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsAccordions.tsx @@ -0,0 +1,125 @@ +/* + * 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, { useContext } from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Divider, + Grid, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { V1Job, V1Pod } from '@kubernetes/client-node'; +import { PodsTable } from '../Pods'; +import { JobDrawer } from './JobsDrawer'; +import { getOwnedResources } from '../../utils/owner'; +import { GroupedResponsesContext } from '../../hooks'; +import { + StatusError, + StatusOK, + StatusPending, +} from '@backstage/core-components'; + +type JobsAccordionsProps = { + jobs: V1Job[]; + children?: React.ReactNode; +}; + +type JobAccordionProps = { + job: V1Job; + ownedPods: V1Pod[]; + children?: React.ReactNode; +}; + +type JobSummaryProps = { + job: V1Job; + children?: React.ReactNode; +}; + +const JobSummary = ({ job }: JobSummaryProps) => { + return ( + + + + + + + + + + {job.status?.succeeded && Succeeded} + {job.status?.active && Running} + {job.status?.failed && Failed} + + Start time: {job.status?.startTime?.toString()} + {job.status?.completionTime && ( + + Completion time: {job.status.completionTime.toString()} + + )} + + + ); +}; + +const JobAccordion = ({ job, ownedPods }: JobAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +export const JobsAccordions = ({ jobs }: JobsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + + return ( + + {jobs.map((job, i) => ( + + + + + + ))} + + ); +}; diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.test.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.test.tsx new file mode 100644 index 0000000000..48a4e6fe7e --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 from 'react'; +import * as oneCronJobsFixture from '../../__fixtures__/1-cronjobs.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { JobDrawer } from './JobsDrawer'; + +describe('JobDrawer', () => { + it('should render job drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + , + ); + + expect(getAllByText('dice-roller-cronjob-1637025000')).toHaveLength(2); + expect(getAllByText('Job')).toHaveLength(2); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('Parallelism')).toBeInTheDocument(); + expect(getByText('Completions')).toBeInTheDocument(); + expect(getByText('Backoff Limit')).toBeInTheDocument(); + expect(getByText('Start Time')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx new file mode 100644 index 0000000000..a7217725b2 --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/JobsDrawer.tsx @@ -0,0 +1,62 @@ +/* + * 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 from 'react'; +import { V1Job } from '@kubernetes/client-node'; +import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer'; +import { Typography, Grid } from '@material-ui/core'; + +export const JobDrawer = ({ + job, + expanded, +}: { + job: V1Job; + expanded?: boolean; +}) => { + return ( + { + return { + parallelism: jobObj.spec?.parallelism ?? '???', + completions: jobObj.spec?.completions ?? '???', + backoffLimit: jobObj.spec?.backoffLimit ?? '???', + startTime: jobObj.status?.startTime ?? '???', + }; + }} + > + + + + {job.metadata?.name ?? 'unknown object'} + + + + + Job + + + + + ); +}; diff --git a/plugins/kubernetes/src/components/JobsAccordions/index.ts b/plugins/kubernetes/src/components/JobsAccordions/index.ts new file mode 100644 index 0000000000..392309de79 --- /dev/null +++ b/plugins/kubernetes/src/components/JobsAccordions/index.ts @@ -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 { JobsAccordions } from './JobsAccordions'; diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx index 7c6c7e97f7..868b74bbc9 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.test.tsx @@ -18,8 +18,25 @@ import React from 'react'; import { render } from '@testing-library/react'; import * as pod from './__fixtures__/pod.json'; import * as crashingPod from './__fixtures__/crashing-pod.json'; +import { TableColumn } from '@backstage/core-components'; import { wrapInTestApp } from '@backstage/test-utils'; +import { V1Pod } from '@kubernetes/client-node'; import { PodsTable } from './PodsTable'; +import { containersReady, totalRestarts } from '../../utils/pod'; + +const extraColumns: TableColumn[] = [ + { + title: 'containers ready', + align: 'center', + render: containersReady, + }, + { + title: 'total restarts', + align: 'center', + render: totalRestarts, + type: 'numeric', + }, +]; describe('PodsTable', () => { it('should render pod', async () => { @@ -27,6 +44,24 @@ describe('PodsTable', () => { wrapInTestApp(), ); + // titles + expect(getByText('name')).toBeInTheDocument(); + expect(getByText('phase')).toBeInTheDocument(); + expect(getByText('status')).toBeInTheDocument(); + + // values + expect(getByText('dice-roller-6c8646bfd-2m5hv')).toBeInTheDocument(); + expect(getByText('Running')).toBeInTheDocument(); + expect(getByText('OK')).toBeInTheDocument(); + }); + + it('should render pod with extra columns', async () => { + const { getByText } = render( + wrapInTestApp( + , + ), + ); + // titles expect(getByText('name')).toBeInTheDocument(); expect(getByText('phase')).toBeInTheDocument(); @@ -41,9 +76,12 @@ describe('PodsTable', () => { expect(getByText('0')).toBeInTheDocument(); expect(getByText('OK')).toBeInTheDocument(); }); - it('should render crashing pod', async () => { + + it('should render crashing pod with extra columns', async () => { const { getByText, getAllByText } = render( - wrapInTestApp(), + wrapInTestApp( + , + ), ); // titles diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx index 7b5497ff37..8ce5f813b3 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx @@ -17,14 +17,10 @@ import React from 'react'; import { V1Pod } from '@kubernetes/client-node'; import { PodDrawer } from './PodDrawer'; -import { - containersReady, - containerStatuses, - totalRestarts, -} from '../../utils/pod'; +import { containerStatuses } from '../../utils/pod'; import { Table, TableColumn } from '@backstage/core-components'; -const columns: TableColumn[] = [ +const DEFAULT_COLUMNS: TableColumn[] = [ { title: 'name', highlight: true, @@ -34,29 +30,19 @@ const columns: TableColumn[] = [ title: 'phase', render: (pod: V1Pod) => pod.status?.phase ?? 'unknown', }, - { - title: 'containers ready', - align: 'center', - render: containersReady, - }, - { - title: 'total restarts', - align: 'center', - render: totalRestarts, - type: 'numeric', - }, { title: 'status', render: containerStatuses, }, ]; -type DeploymentTablesProps = { +type PodsTablesProps = { pods: V1Pod[]; + extraColumns?: TableColumn[]; children?: React.ReactNode; }; -export const PodsTable = ({ pods }: DeploymentTablesProps) => { +export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => { const tableStyle = { minWidth: '0', width: '100%', @@ -67,7 +53,7 @@ export const PodsTable = ({ pods }: DeploymentTablesProps) => {
); diff --git a/plugins/kubernetes/src/hooks/GroupedResponses.ts b/plugins/kubernetes/src/hooks/GroupedResponses.ts index d000086b7c..35f7e10b5c 100644 --- a/plugins/kubernetes/src/hooks/GroupedResponses.ts +++ b/plugins/kubernetes/src/hooks/GroupedResponses.ts @@ -24,5 +24,7 @@ export const GroupedResponsesContext = React.createContext({ configMaps: [], horizontalPodAutoscalers: [], ingresses: [], + jobs: [], + cronJobs: [], customResources: [], }); diff --git a/plugins/kubernetes/src/types/types.ts b/plugins/kubernetes/src/types/types.ts index 05337086be..d6df99cb95 100644 --- a/plugins/kubernetes/src/types/types.ts +++ b/plugins/kubernetes/src/types/types.ts @@ -22,6 +22,8 @@ import { V1Service, V1ConfigMap, ExtensionsV1beta1Ingress, + V1Job, + V1CronJob, } from '@kubernetes/client-node'; export interface DeploymentResources { @@ -35,6 +37,8 @@ export interface GroupedResponses extends DeploymentResources { services: V1Service[]; configMaps: V1ConfigMap[]; ingresses: ExtensionsV1beta1Ingress[]; + jobs: V1Job[]; + cronJobs: V1CronJob[]; customResources: any[]; } diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts index afc53d9509..31e07a0f14 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.test.ts @@ -78,21 +78,6 @@ describe('clusterLinks', () => { 'https://k8s.foo.com/#/service/bar/foobar?namespace=bar', ); }); - it('should return an url on the deployment properly url encoded', () => { - const url = formatClusterLink({ - dashboardUrl: 'https://k8s.foo.com/', - object: { - metadata: { - name: 'foobar', - namespace: 'bar bar', - }, - }, - kind: 'Deployment', - }); - expect(url).toBe( - 'https://k8s.foo.com/#/deployment/bar%20bar/foobar?namespace=bar+bar', - ); - }); }); describe('standard app', () => { diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts index 83e970248b..a8f83a9c76 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatClusterLink.ts @@ -40,8 +40,5 @@ export function formatClusterLink(options: FormatClusterLinkOptions) { object: options.object, kind: options.kind, }); - // Note that we can't rely on 'url.href' since it will put the search before the hash - // and this won't be properly recognized by SPAs such as Angular in the standard dashboard. - // Note also that pathname, hash and search will be properly url encoded. - return `${url.origin}${url.pathname}${url.hash}${url.search}`; + return url.toString(); } diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts index df2529a8ac..f0b85cad49 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.test.ts @@ -17,19 +17,126 @@ import { openshiftFormatter } from './openshift'; describe('clusterLinks - OpenShift formatter', () => { it('should return an url on the workloads when there is a namespace only', () => { - expect(() => - openshiftFormatter({ - dashboardUrl: new URL('https://k8s.foo.com'), - object: { - metadata: { - name: 'foobar', - namespace: 'bar', - }, + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + namespace: 'bar', }, - kind: 'Deployment', - }), - ).toThrowError( - 'OpenShift formatter is not yet implemented. Please, contribute!', + }, + kind: 'foo', + }); + expect(url.href).toBe('https://k8s.foo.com/k8s/cluster/projects/bar'); + }); + it('should return an url on the workloads when the kind is not recognizeed', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'UnknownKind', + }); + expect(url.href).toBe('https://k8s.foo.com/k8s/cluster/projects/bar'); + }); + it('should return an url on the deployment', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(url.href).toBe('https://k8s.foo.com/k8s/ns/bar/deployments/foobar'); + }); + it('should return an url on the deployment and keep the path prefix 1', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/some/prefix/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(url.href).toBe( + 'https://k8s.foo.com/some/prefix/k8s/ns/bar/deployments/foobar', + ); + }); + it('should return an url on the deployment and keep the path prefix 2', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/some/prefix'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(url.href).toBe( + 'https://k8s.foo.com/some/prefix/k8s/ns/bar/deployments/foobar', + ); + }); + it('should return an url on the service', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Service', + }); + expect(url.href).toBe('https://k8s.foo.com/k8s/ns/bar/services/foobar'); + }); + it('should return an url on the ingress', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Ingress', + }); + expect(url.href).toBe('https://k8s.foo.com/k8s/ns/bar/ingresses/foobar'); + }); + it('should return an url on the deployment for a hpa', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'HorizontalPodAutoscaler', + }); + expect(url.href).toBe( + 'https://k8s.foo.com/k8s/ns/bar/horizontalpodautoscalers/foobar', + ); + }); + it('should return an url on the PV', () => { + const url = openshiftFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + }, + }, + kind: 'PersistentVolume', + }); + expect(url.href).toBe( + 'https://k8s.foo.com/k8s/cluster/persistentvolumes/foobar', ); }); }); diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts index bacb747ebb..6c20cd4720 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/openshift.ts @@ -15,10 +15,40 @@ */ import { ClusterLinksFormatterOptions } from '../../../types/types'; -export function openshiftFormatter( - _options: ClusterLinksFormatterOptions, -): URL { - throw new Error( - 'OpenShift formatter is not yet implemented. Please, contribute!', +const kindMappings: Record = { + deployment: 'deployments', + ingress: 'ingresses', + service: 'services', + horizontalpodautoscaler: 'horizontalpodautoscalers', + persistentvolume: 'persistentvolumes', +}; + +export function openshiftFormatter(options: ClusterLinksFormatterOptions): URL { + const basePath = new URL(options.dashboardUrl.href); + const name = encodeURIComponent(options.object.metadata?.name ?? ''); + const namespace = encodeURIComponent( + options.object.metadata?.namespace ?? '', ); + const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')]; + if (!basePath.pathname.endsWith('/')) { + // a dashboard url with a path should end with a slash otherwise + // the new combined URL will replace the last segment with the appended path! + // https://foobar.com/abc/def + k8s/cluster/projects/test --> https://foobar.com/abc/k8s/cluster/projects/test + // https://foobar.com/abc/def/ + k8s/cluster/projects/test --> https://foobar.com/abc/def/k8s/cluster/projects/test + basePath.pathname += '/'; + } + let path = ''; + if (namespace) { + if (name && validKind) { + path = `k8s/ns/${namespace}/${validKind}/${name}`; + } else { + path = `k8s/cluster/projects/${namespace}`; + } + } else if (validKind) { + path = `k8s/cluster/${validKind}`; + if (name) { + path += `/${name}`; + } + } + return new URL(path, basePath); } diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts index a3a274496e..1ace39ec59 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/rancher.ts @@ -23,14 +23,24 @@ const kindMappings: Record = { }; export function rancherFormatter(options: ClusterLinksFormatterOptions): URL { - const result = new URL(options.dashboardUrl.href); - const name = options.object.metadata?.name; - const namespace = options.object.metadata?.namespace; + const basePath = new URL(options.dashboardUrl.href); + const name = encodeURIComponent(options.object.metadata?.name ?? ''); + const namespace = encodeURIComponent( + options.object.metadata?.namespace ?? '', + ); const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')]; - if (validKind && name && namespace) { - result.pathname += `explorer/${validKind}/${namespace}/${name}`; - } else if (namespace) { - result.pathname += 'explorer/workload'; + if (!basePath.pathname.endsWith('/')) { + // a dashboard url with a path should end with a slash otherwise + // the new combined URL will replace the last segment with the appended path! + // https://foobar.com/abc/def + explorer/service/test --> https://foobar.com/abc/explorer/service/test + // https://foobar.com/abc/def/ + explorer/service/test --> https://foobar.com/abc/def/explorer/service/test + basePath.pathname += '/'; } - return result; + let path = ''; + if (validKind && name && namespace) { + path = `explorer/${validKind}/${namespace}/${name}`; + } else if (namespace) { + path = 'explorer/workload'; + } + return new URL(path, basePath); } diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts index 76bbf5643d..0b3c21a627 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.test.ts @@ -67,6 +67,51 @@ describe('clusterLinks - standard formatter', () => { 'https://k8s.foo.com/#/deployment/bar/foobar?namespace=bar', ); }); + it('should return an url on the deployment with a prefix 1', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/some/prefix'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/some/prefix/#/deployment/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the deployment with a prefix 2', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/some/prefix/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar', + }, + }, + kind: 'Deployment', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/some/prefix/#/deployment/bar/foobar?namespace=bar', + ); + }); + it('should return an url on the deployment properly url encoded', () => { + const url = standardFormatter({ + dashboardUrl: new URL('https://k8s.foo.com/'), + object: { + metadata: { + name: 'foobar', + namespace: 'bar bar', + }, + }, + kind: 'Deployment', + }); + expect(formatUrl(url)).toBe( + 'https://k8s.foo.com/#/deployment/bar%20bar/foobar?namespace=bar%20bar', + ); + }); it('should return an url on the service', () => { const url = standardFormatter({ dashboardUrl: new URL('https://k8s.foo.com/'), diff --git a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts index fb26cf220d..e28c9fae2b 100644 --- a/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts +++ b/plugins/kubernetes/src/utils/clusterLinks/formatters/standard.ts @@ -24,16 +24,22 @@ const kindMappings: Record = { export function standardFormatter(options: ClusterLinksFormatterOptions) { const result = new URL(options.dashboardUrl.href); - const name = options.object.metadata?.name; - const namespace = options.object.metadata?.namespace; + const name = encodeURIComponent(options.object.metadata?.name ?? ''); + const namespace = encodeURIComponent( + options.object.metadata?.namespace ?? '', + ); const validKind = kindMappings[options.kind.toLocaleLowerCase('en-US')]; - if (namespace) { - result.searchParams.set('namespace', namespace); + if (!result.pathname.endsWith('/')) { + result.pathname += '/'; } if (validKind && name && namespace) { result.hash = `/${validKind}/${namespace}/${name}`; } else if (namespace) { result.hash = '/workloads'; } + if (namespace) { + // Note that Angular SPA requires a hash and the query parameter should be part of it + result.hash += `?namespace=${namespace}`; + } return result; } diff --git a/plugins/kubernetes/src/utils/response.ts b/plugins/kubernetes/src/utils/response.ts index 97501bb102..cfe3d20580 100644 --- a/plugins/kubernetes/src/utils/response.ts +++ b/plugins/kubernetes/src/utils/response.ts @@ -45,6 +45,12 @@ export const groupResponses = ( case 'ingresses': prev.ingresses.push(...next.resources); break; + case 'jobs': + prev.jobs.push(...next.resources); + break; + case 'cronjobs': + prev.cronJobs.push(...next.resources); + break; case 'customresources': prev.customResources.push(...next.resources); break; @@ -60,6 +66,8 @@ export const groupResponses = ( configMaps: [], horizontalPodAutoscalers: [], ingresses: [], + jobs: [], + cronJobs: [], customResources: [], } as GroupedResponses, ); diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index e53f6ee261..009252b774 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,8 +34,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index 1433a186fb..53de33d21b 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -30,8 +30,9 @@ import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { AuditListForEntity } from './AuditListForEntity'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiRegistry } from '@backstage/test-utils'; jest.mock('../../hooks/useWebsiteForEntity', () => ({ useWebsiteForEntity: jest.fn(), @@ -41,7 +42,7 @@ const websiteListResponse = data as WebsiteListResponse; const entityWebsite = websiteListResponse.items[0]; describe('', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; const mockErrorApi: jest.Mocked = { post: jest.fn(), @@ -49,10 +50,10 @@ describe('', () => { }; beforeEach(() => { - apis = ApiRegistry.from([ + apis = TestApiRegistry.from( [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], [errorApiRef, mockErrorApi], - ]); + ); (useWebsiteForEntity as jest.Mock).mockReturnValue({ value: entityWebsite, diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index 4c92997755..d8a54928b2 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -16,7 +16,11 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInTestApp, setupRequestMockHandlers } from '@backstage/test-utils'; +import { + wrapInTestApp, + setupRequestMockHandlers, + TestApiRegistry, +} from '@backstage/test-utils'; import AuditListTable from './AuditListTable'; import { @@ -28,19 +32,20 @@ import { formatTime } from '../../utils'; import { setupServer } from 'msw/node'; import * as data from '../../__fixtures__/website-list-response.json'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const websiteListResponse = data as WebsiteListResponse; describe('AuditListTable', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; const server = setupServer(); setupRequestMockHandlers(server); beforeEach(() => { - apis = ApiRegistry.from([ - [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], + apis = TestApiRegistry.from([ + lighthouseApiRef, + new LighthouseRestApi('http://lighthouse'), ]); }); diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index ea3587fea9..2b422cce7a 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -23,7 +23,11 @@ jest.mock('react-router-dom', () => { }; }); -import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { fireEvent, render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -35,20 +39,21 @@ import { } from '../../api'; import * as data from '../../__fixtures__/website-list-response.json'; import AuditList from './index'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const { useNavigate } = jest.requireMock('react-router-dom'); const websiteListResponse = data as WebsiteListResponse; describe('AuditList', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; const server = setupServer(); setupRequestMockHandlers(server); beforeEach(() => { - apis = ApiRegistry.from([ - [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], + apis = TestApiRegistry.from([ + lighthouseApiRef, + new LighthouseRestApi('http://lighthouse'), ]); }); diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index ed1bbbcaf9..00b2843a50 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -26,7 +26,11 @@ jest.mock('react-router-dom', () => { }; }); -import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -35,14 +39,14 @@ import { Audit, lighthouseApiRef, LighthouseRestApi, Website } from '../../api'; import { formatTime } from '../../utils'; import * as data from '../../__fixtures__/website-response.json'; import AuditView from './index'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const { useParams }: { useParams: jest.Mock } = jest.requireMock('react-router-dom'); const websiteResponse = data as Website; describe('AuditView', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; let id: string; const server = setupServer(); @@ -55,8 +59,9 @@ describe('AuditView', () => { ), ); - apis = ApiRegistry.from([ - [lighthouseApiRef, new LighthouseRestApi('https://lighthouse')], + apis = TestApiRegistry.from([ + lighthouseApiRef, + new LighthouseRestApi('https://lighthouse'), ]); id = websiteResponse.audits.find(a => a.status === 'COMPLETED') ?.id as string; diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index 84493e243f..59847b516e 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -23,7 +23,11 @@ jest.mock('react-router-dom', () => { }; }); -import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -32,7 +36,7 @@ import { Audit, lighthouseApiRef, LighthouseRestApi } from '../../api'; import * as data from '../../__fixtures__/create-audit-response.json'; import CreateAudit from './index'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { ErrorApi, errorApiRef } from '@backstage/core-plugin-api'; const { useNavigate }: { useNavigate: jest.Mock } = @@ -41,17 +45,17 @@ const createAuditResponse = data as Audit; // TODO add act() to these tests without breaking them! describe('CreateAudit', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; let errorApi: ErrorApi; const server = setupServer(); setupRequestMockHandlers(server); beforeEach(() => { errorApi = { post: jest.fn(), error$: jest.fn() }; - apis = ApiRegistry.from([ + apis = TestApiRegistry.from( [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], [errorApiRef, errorApi], - ]); + ); }); it('renders the form', () => { diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index 64f7483081..13da0f6f4f 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -21,8 +21,8 @@ import { lighthouseApiRef, WebsiteListResponse } from '../api'; import * as data from '../__fixtures__/website-list-response.json'; import { useWebsiteForEntity } from './useWebsiteForEntity'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; const websiteListResponse = data as WebsiteListResponse; const website = websiteListResponse.items[0]; @@ -55,14 +55,14 @@ describe('useWebsiteForEntity', () => { const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( - {children} - + ); }; diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 16029f1783..bd5e57ae66 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index f46095f46a..37c3320810 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-org +## 0.3.29 + +### Patch Changes + +- 2f4a686411: Use email links in the catalog's members list instead of text to display a member's email +- Updated dependencies + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.3.28 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index db6cfe4d0b..a9146c8f7a 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.3.28", + "version": "0.3.29", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -37,10 +37,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx index 0a5a0cfef1..b6b549d934 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx @@ -15,8 +15,8 @@ */ import { Entity, GroupEntity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; +import { TestApiProvider } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; import React from 'react'; import { MemoryRouter } from 'react-router'; @@ -99,12 +99,9 @@ const catalogApi = (items: Entity[]) => ({ getEntities: () => Promise.resolve({ items }), }); -const apiRegistry = (items: Entity[]) => - ApiRegistry.from([[catalogApiRef, catalogApi(items)]]); - export const Default = () => ( - + @@ -112,13 +109,13 @@ export const Default = () => ( - + ); export const Empty = () => ( - + @@ -126,6 +123,6 @@ export const Empty = () => ( - + ); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 75cb9efd7f..b0c68d9a83 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -20,10 +20,13 @@ import { catalogApiRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('MemberTab Test', () => { const groupEntity: GroupEntity = { @@ -103,17 +106,15 @@ describe('MemberTab Test', () => { }), }; - const apis = ApiRegistry.from([[catalogApiRef, catalogApi]]); - it('Display Profile Card', async () => { const rendered = await renderWithEffects( wrapInTestApp( - + , - , + , ), ); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 0642e80a7b..7a15bd2122 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -28,14 +28,13 @@ import { Box, createStyles, Grid, - Link, makeStyles, Theme, Typography, } from '@material-ui/core'; import Pagination from '@material-ui/lab/Pagination'; import React from 'react'; -import { generatePath, Link as RouterLink } from 'react-router-dom'; +import { generatePath } from 'react-router-dom'; import { useAsync } from 'react-use'; import { @@ -43,6 +42,7 @@ import { InfoCard, Progress, ResponseErrorPanel, + Link, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; @@ -90,7 +90,6 @@ const MemberComponent = ({ member }: { member: UserEntity }) => { { {displayName} - {profile?.email} + {profile?.email && ( + {profile.email} + )} diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx index 14fed985b6..6a20035dfa 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx @@ -15,14 +15,14 @@ */ import { GroupEntity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { CatalogApi, catalogApiRef, catalogRouteRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { BackstageTheme, createTheme, @@ -86,11 +86,11 @@ const catalogApi: Partial = { getEntities: () => Promise.resolve({ items: [serviceA, serviceB, websiteA] }), }; -const apiRegistry = ApiRegistry.from([[catalogApiRef, catalogApi]]); +const apis = TestApiRegistry.from([catalogApiRef, catalogApi]); export const Default = () => wrapInTestApp( - + @@ -123,7 +123,7 @@ const monochromeTheme = (outer: BackstageTheme) => export const Themed = () => wrapInTestApp( - + diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index b5ffd37083..8d6fc1a002 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -21,11 +21,10 @@ import { EntityProvider, catalogRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { queryByText } from '@testing-library/react'; import React from 'react'; import { OwnershipCard } from './OwnershipCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('OwnershipCard', () => { const groupEntity: GroupEntity = { @@ -121,11 +120,11 @@ describe('OwnershipCard', () => { }); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, @@ -157,11 +156,11 @@ describe('OwnershipCard', () => { }); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, @@ -205,11 +204,11 @@ describe('OwnershipCard', () => { }); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 7981b2c104..d02c2df57b 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx index 446c867f35..723e7b7b19 100644 --- a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx +++ b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { ChangeEvent } from '../types'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { ChangeEvents } from './ChangeEvents'; const mockPagerDutyApi = { - getChangeEventsByServiceId: () => [], + getChangeEventsByServiceId: jest.fn(), }; -const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); +const apis = TestApiRegistry.from([pagerDutyApiRef, mockPagerDutyApi]); describe('Incidents', () => { it('Renders an empty state when there are no change events', async () => { diff --git a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx index 52aa9a860f..47f18002fe 100644 --- a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx +++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { EscalationPolicy } from './EscalationPolicy'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { User } from '../types'; import { pagerDutyApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi = { - getOnCallByPolicyId: () => [], + getOnCallByPolicyId: jest.fn(), }; -const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); +const apis = TestApiRegistry.from([pagerDutyApiRef, mockPagerDutyApi]); describe('Escalation', () => { it('Handles an empty response', async () => { diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index d66acc0879..74e7d82e8b 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { Incidents } from './Incidents'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; import { Incident } from '../types'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi = { - getIncidentsByServiceId: () => [], + getIncidentsByServiceId: jest.fn(), }; -const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); +const apis = TestApiRegistry.from([pagerDutyApiRef, mockPagerDutyApi]); describe('Incidents', () => { it('Renders an empty state when there are no incidents', async () => { diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index d644aa019a..e0a857ccc7 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -18,12 +18,12 @@ import { render, waitFor, fireEvent, act } from '@testing-library/react'; import { PagerDutyCard } from '../PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api'; import { Service, User } from '../types'; -import { alertApiRef, createApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { alertApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi: Partial = { getServiceByIntegrationKey: async () => [], @@ -31,16 +31,10 @@ const mockPagerDutyApi: Partial = { getIncidentsByServiceId: async () => [], }; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [pagerDutyApiRef, mockPagerDutyApi], - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], -]); + [alertApiRef, {}], +); const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', diff --git a/plugins/pagerduty/src/components/TriggerButton/index.test.tsx b/plugins/pagerduty/src/components/TriggerButton/index.test.tsx index 21f0dd5f1d..3776bcaf1c 100644 --- a/plugins/pagerduty/src/components/TriggerButton/index.test.tsx +++ b/plugins/pagerduty/src/components/TriggerButton/index.test.tsx @@ -15,16 +15,15 @@ */ import React from 'react'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TriggerButton } from './'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef, - createApiRef, IdentityApi, identityApiRef, } from '@backstage/core-plugin-api'; @@ -39,17 +38,11 @@ describe('TriggerButton', () => { triggerAlarm: mockTriggerAlarmFn, }; - const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], + const apis = TestApiRegistry.from( + [alertApiRef, {}], [identityApiRef, mockIdentityApi], [pagerDutyApiRef, mockPagerDutyApi], - ]); + ); it('renders the trigger button, opens and closes dialog', async () => { const entity: Entity = { diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index 444e820b98..d6f378b4bf 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -15,16 +15,15 @@ */ import React from 'react'; import { fireEvent, act } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TriggerDialog } from './TriggerDialog'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef, - createApiRef, IdentityApi, identityApiRef, } from '@backstage/core-plugin-api'; @@ -39,17 +38,11 @@ describe('TriggerDialog', () => { triggerAlarm: mockTriggerAlarmFn, }; - const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], + const apis = TestApiRegistry.from( + [alertApiRef, {}], [identityApiRef, mockIdentityApi], [pagerDutyApiRef, mockPagerDutyApi], - ]); + ); it('open the dialog and trigger an alarm', async () => { const entity: Entity = { diff --git a/plugins/permission-backend/.eslintrc.js b/plugins/permission-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/permission-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md new file mode 100644 index 0000000000..5df0a5826a --- /dev/null +++ b/plugins/permission-backend/CHANGELOG.md @@ -0,0 +1,15 @@ +# @backstage/plugin-permission-backend + +## 0.1.0 + +### Minor Changes + +- 7a8312f126: New package containing the backend for authorization and permissions. For more information, see the [authorization PRFC](https://github.com/backstage/backstage/pull/7761). + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.4.9 + - @backstage/plugin-permission-node@0.1.0 + - @backstage/backend-common@0.9.11 + - @backstage/plugin-permission-common@0.2.0 diff --git a/plugins/permission-backend/README.md b/plugins/permission-backend/README.md new file mode 100644 index 0000000000..59fe1730ba --- /dev/null +++ b/plugins/permission-backend/README.md @@ -0,0 +1,6 @@ +# @backstage/plugin-permission-backend + +> NOTE: THIS PACKAGE IS EXPERIMENTAL, HERE BE DRAGONS + +Backend for Backstage authorization and permissions. For more information, see +the [authorization PRFC](https://github.com/backstage/backstage/pull/7761). diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md new file mode 100644 index 0000000000..4857c8b044 --- /dev/null +++ b/plugins/permission-backend/api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/plugin-permission-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import express from 'express'; +import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { Logger as Logger_2 } from 'winston'; +import { PermissionPolicy } from '@backstage/plugin-permission-node'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public +export function createRouter(options: RouterOptions): Promise; + +// @public +export interface RouterOptions { + // (undocumented) + discovery: PluginEndpointDiscovery; + // (undocumented) + identity: IdentityClient; + // (undocumented) + logger: Logger_2; + // (undocumented) + policy: PermissionPolicy; +} +``` diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json new file mode 100644 index 0000000000..f335079d0b --- /dev/null +++ b/plugins/permission-backend/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/plugin-permission-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.9.11", + "@backstage/config": "^0.1.11", + "@backstage/plugin-auth-backend": "^0.4.9", + "@backstage/plugin-permission-common": "^0.2.0", + "@backstage/plugin-permission-node": "^0.1.0", + "@types/express": "*", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "node-fetch": "^2.6.1", + "winston": "^3.2.1", + "yn": "^4.0.0", + "zod": "^3.11.6" + }, + "devDependencies": { + "@backstage/cli": "^0.9.1", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2", + "msw": "^0.35.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/permission-backend/src/index.ts b/plugins/permission-backend/src/index.ts new file mode 100644 index 0000000000..877b9070c3 --- /dev/null +++ b/plugins/permission-backend/src/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Backend for Backstage authorization and permissions. + * @packageDocumentation + */ +export * from './service'; diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts new file mode 100644 index 0000000000..6a35488117 --- /dev/null +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -0,0 +1,292 @@ +/* + * 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 { AddressInfo } from 'net'; +import { Server } from 'http'; +import express, { Router } from 'express'; +import { RestContext, rest } from 'msw'; +import { setupServer, SetupServerApi } from 'msw/node'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +import { PermissionIntegrationClient } from './PermissionIntegrationClient'; + +describe('PermissionIntegrationClient', () => { + describe('applyConditions', () => { + let server: SetupServerApi; + + const mockConditions = { + not: { + allOf: [ + { rule: 'RULE_1', params: [] }, + { rule: 'RULE_2', params: ['abc'] }, + ], + }, + }; + + const mockApplyConditionsHandler = jest.fn( + (_req, res, { json }: RestContext) => { + return res(json({ result: AuthorizeResult.ALLOW })); + }, + ); + + const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; + const discovery: PluginEndpointDiscovery = { + async getBaseUrl() { + return mockBaseUrl; + }, + async getExternalBaseUrl() { + throw new Error('Not implemented.'); + }, + }; + + const client: PermissionIntegrationClient = new PermissionIntegrationClient( + { + discovery, + }, + ); + + beforeAll(() => { + server = setupServer(); + server.listen({ onUnhandledRequest: 'error' }); + server.use( + rest.post( + `${mockBaseUrl}/permissions/apply-conditions`, + mockApplyConditionsHandler, + ), + ); + }); + + afterAll(() => server.close()); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should make a POST request to the correct endpoint', async () => { + await client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }); + + expect(mockApplyConditionsHandler).toHaveBeenCalled(); + }); + + it('should include a request body', async () => { + await client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }); + + expect(mockApplyConditionsHandler).toHaveBeenCalledWith( + expect.objectContaining({ + body: { + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }, + }), + expect.anything(), + expect.anything(), + ); + }); + + it('should return the response from the fetch request', async () => { + const response = await client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }); + + expect(response).toEqual( + expect.objectContaining({ result: AuthorizeResult.ALLOW }), + ); + }); + + it('should not include authorization headers if no token is supplied', async () => { + await client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }); + + const request = mockApplyConditionsHandler.mock.calls[0][0]; + expect(request.headers.has('authorization')).toEqual(false); + }); + + it('should include correctly-constructed authorization header if token is supplied', async () => { + await client.applyConditions( + { + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }, + 'Bearer fake-token', + ); + + const request = mockApplyConditionsHandler.mock.calls[0][0]; + expect(request.headers.get('authorization')).toEqual('Bearer fake-token'); + }); + + it('should forward response errors', async () => { + mockApplyConditionsHandler.mockImplementationOnce( + (_req, res, { status }: RestContext) => { + return res(status(401)); + }, + ); + + await expect( + client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }), + ).rejects.toThrowError(/401/i); + }); + + it('should reject invalid responses', async () => { + mockApplyConditionsHandler.mockImplementationOnce( + (_req, res, { json }: RestContext) => { + return res(json({ outcome: AuthorizeResult.ALLOW })); + }, + ); + + await expect( + client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: mockConditions, + }), + ).rejects.toThrowError(/invalid input/i); + }); + }); + + describe('integration with @backstage/plugin-permission-node', () => { + let server: Server; + let client: PermissionIntegrationClient; + + beforeAll(async () => { + const router = Router(); + + router.use( + createPermissionIntegrationRouter({ + resourceType: 'test-resource', + getResource: async resourceRef => ({ id: resourceRef }), + rules: [ + { + name: 'RULE_1', + description: 'Test rule 1', + apply: (_resource: any, input: 'yes' | 'no') => input === 'yes', + toQuery: () => { + throw new Error('Not implemented'); + }, + }, + { + name: 'RULE_2', + description: 'Test rule 2', + apply: (_resource: any, input: 'yes' | 'no') => input === 'yes', + toQuery: () => { + throw new Error('Not implemented'); + }, + }, + ], + }), + ); + + const app = express(); + + app.use('/test-plugin', router); + + await new Promise(resolve => { + server = app.listen(resolve); + }); + + const discovery: PluginEndpointDiscovery = { + async getBaseUrl(pluginId: string) { + const listenPort = (server.address()! as AddressInfo).port; + + return `http://0.0.0.0:${listenPort}/${pluginId}`; + }, + async getExternalBaseUrl() { + throw new Error('Not implemented.'); + }, + }; + + client = new PermissionIntegrationClient({ + discovery, + }); + }); + + afterAll( + async () => + new Promise((resolve, reject) => + server.close(err => (err ? reject(err) : resolve())), + ), + ); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('works for simple conditions', async () => { + await expect( + client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: { rule: 'RULE_1', params: ['no'] }, + }), + ).resolves.toEqual({ result: AuthorizeResult.DENY }); + }); + + it('works for complex criteria', async () => { + await expect( + client.applyConditions({ + pluginId: 'test-plugin', + resourceRef: 'testResource1', + resourceType: 'test-resource', + conditions: { + allOf: [ + { + allOf: [ + { rule: 'RULE_1', params: ['yes'] }, + { not: { rule: 'RULE_2', params: ['no'] } }, + ], + }, + { + not: { + allOf: [ + { rule: 'RULE_1', params: ['no'] }, + { rule: 'RULE_2', params: ['yes'] }, + ], + }, + }, + ], + }, + }), + ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); + }); + }); +}); diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts new file mode 100644 index 0000000000..0d6017cfd3 --- /dev/null +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.ts @@ -0,0 +1,82 @@ +/* + * 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 fetch from 'node-fetch'; +import { z } from 'zod'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + AuthorizeResult, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { + ApplyConditionsRequest, + ApplyConditionsResponse, +} from '@backstage/plugin-permission-node'; + +const responseSchema = z.object({ + result: z.literal(AuthorizeResult.ALLOW).or(z.literal(AuthorizeResult.DENY)), +}); + +export class PermissionIntegrationClient { + private readonly discovery: PluginEndpointDiscovery; + + constructor(options: { discovery: PluginEndpointDiscovery }) { + this.discovery = options.discovery; + } + + async applyConditions( + { + pluginId, + resourceRef, + resourceType, + conditions, + }: { + resourceRef: string; + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }, + authHeader?: string, + ): Promise { + const endpoint = `${await this.discovery.getBaseUrl( + pluginId, + )}/permissions/apply-conditions`; + + const request: ApplyConditionsRequest = { + resourceRef, + resourceType, + conditions, + }; + + const response = await fetch(endpoint, { + method: 'POST', + body: JSON.stringify(request), + headers: { + ...(authHeader ? { authorization: authHeader } : {}), + 'content-type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error( + `Unexpected response from plugin upstream when applying conditions. Expected 200 but got ${response.status} - ${response.statusText}`, + ); + } + + return responseSchema.parse(await response.json()); + } +} diff --git a/plugins/permission-backend/src/service/index.ts b/plugins/permission-backend/src/service/index.ts new file mode 100644 index 0000000000..3ce8cdbcc4 --- /dev/null +++ b/plugins/permission-backend/src/service/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { createRouter } from './router'; +export type { RouterOptions } from './router'; diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts new file mode 100644 index 0000000000..26fa7ef8a6 --- /dev/null +++ b/plugins/permission-backend/src/service/router.test.ts @@ -0,0 +1,294 @@ +/* + * 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 express from 'express'; +import request from 'supertest'; +import { getVoidLogger } from '@backstage/backend-common'; +import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { ApplyConditionsResponse } from '@backstage/plugin-permission-node'; +import { PermissionIntegrationClient } from './PermissionIntegrationClient'; + +import { createRouter } from './router'; + +const mockApplyConditions: jest.MockedFunction< + InstanceType['applyConditions'] +> = jest.fn(); +jest.mock('./PermissionIntegrationClient', () => ({ + PermissionIntegrationClient: jest.fn(() => ({ + applyConditions: mockApplyConditions, + })), +})); + +const policy = { + handle: jest.fn().mockImplementation((_req, identity) => { + if (identity) { + return { result: AuthorizeResult.ALLOW }; + } + return { result: AuthorizeResult.DENY }; + }), +}; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + discovery: { + getBaseUrl: jest.fn(), + getExternalBaseUrl: jest.fn(), + }, + identity: { + authenticate: jest.fn(token => { + if (!token) { + throw new Error('No token supplied!'); + } + + return Promise.resolve({ + id: 'test-user', + token, + }); + }), + } as unknown as IdentityClient, + policy, + }); + + app = express().use(router); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); + + describe('POST /authorize', () => { + it('calls the permission policy', async () => { + const response = await request(app) + .post('/authorize') + .send([ + { + id: '123', + permission: { + name: 'test.permission1', + attributes: {}, + }, + }, + { + id: '234', + permission: { + name: 'test.permission2', + attributes: {}, + }, + }, + ]); + + expect(response.status).toEqual(200); + + expect(policy.handle).toHaveBeenCalledWith( + { + permission: { + name: 'test.permission1', + attributes: {}, + }, + }, + undefined, + ); + expect(policy.handle).toHaveBeenCalledWith( + { + permission: { + name: 'test.permission2', + attributes: {}, + }, + }, + undefined, + ); + + expect(response.body).toEqual([ + { id: '123', result: AuthorizeResult.DENY }, + { id: '234', result: AuthorizeResult.DENY }, + ]); + }); + + it('resolves identity from the Authorization header', async () => { + const token = 'test-token'; + const response = await request(app) + .post('/authorize') + .auth(token, { type: 'bearer' }) + .send([ + { + id: '123', + permission: { + name: 'test.permission', + attributes: {}, + }, + }, + ]); + + expect(response.status).toEqual(200); + expect(policy.handle).toHaveBeenCalledWith( + { + permission: { + name: 'test.permission', + attributes: {}, + }, + }, + { id: 'test-user', token: 'test-token' }, + ); + expect(response.body).toEqual([ + { id: '123', result: AuthorizeResult.ALLOW }, + ]); + }); + + describe('conditional policy result', () => { + beforeEach(() => { + policy.handle.mockReturnValueOnce({ + result: AuthorizeResult.CONDITIONAL, + pluginId: 'test-plugin', + resourceType: 'test-resource-1', + conditions: { + anyOf: [{ rule: 'test-rule', params: ['abc'] }], + }, + }); + }); + + it('returns conditions if no resourceRef is supplied', async () => { + const response = await request(app) + .post('/authorize') + .send([ + { + id: '123', + permission: { + name: 'test.permission', + resourceType: 'test-resource-1', + attributes: {}, + }, + }, + ]); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + id: '123', + result: AuthorizeResult.CONDITIONAL, + conditions: { anyOf: [{ rule: 'test-rule', params: ['abc'] }] }, + }, + ]); + }); + + it.each([ + AuthorizeResult.ALLOW, + AuthorizeResult.DENY, + ])( + 'applies conditions and returns %s if resourceRef is supplied', + async result => { + mockApplyConditions.mockResolvedValueOnce({ + result, + }); + + const response = await request(app) + .post('/authorize') + .auth('test-token', { type: 'bearer' }) + .send([ + { + id: '123', + resourceRef: 'test/resource', + permission: { + name: 'test.permission', + resourceType: 'test-resource-1', + attributes: {}, + }, + }, + ]); + + expect(mockApplyConditions).toHaveBeenCalledWith( + { + pluginId: 'test-plugin', + resourceType: 'test-resource-1', + resourceRef: 'test/resource', + conditions: { anyOf: [{ rule: 'test-rule', params: ['abc'] }] }, + }, + 'Bearer test-token', + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + id: '123', + result, + }, + ]); + }, + ); + }); + + it.each([ + undefined, + '', + {}, + [{ permission: { name: 'test.permission', attributes: {} } }], + [{ id: '123' }], + [{ id: '123', permission: { name: 'test.permission' } }], + [{ id: '123', permission: { attributes: { invalid: 'attribute' } } }], + ])('returns a 500 error for invalid request %#', async requestBody => { + const response = await request(app).post('/authorize').send(requestBody); + + expect(response.status).toEqual(500); + expect(response.body).toEqual( + expect.objectContaining({ + error: expect.objectContaining({ + message: expect.stringMatching(/invalid/i), + }), + }), + ); + }); + + it('returns a 500 error if the policy returns a different resourceType', async () => { + policy.handle.mockReturnValueOnce({ + result: AuthorizeResult.CONDITIONAL, + pluginId: 'test-plugin', + resourceType: 'test-resource-2', + conditions: {}, + }); + + const response = await request(app) + .post('/authorize') + .send([ + { + id: '123', + permission: { + name: 'test.permission', + resourceType: 'test-resource-1', + attributes: {}, + }, + }, + ]); + + expect(response.status).toEqual(500); + expect(response.body).toEqual( + expect.objectContaining({ + error: expect.objectContaining({ + message: expect.stringMatching(/invalid resource conditions/i), + }), + }), + ); + }); + }); +}); diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts new file mode 100644 index 0000000000..b85e7feb85 --- /dev/null +++ b/plugins/permission-backend/src/service/router.ts @@ -0,0 +1,165 @@ +/* + * 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 { z } from 'zod'; +import express, { Request, Response } from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { + errorHandler, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { + BackstageIdentity, + IdentityClient, +} from '@backstage/plugin-auth-backend'; +import { + AuthorizeResult, + AuthorizeResponse, + AuthorizeRequest, + Identified, +} from '@backstage/plugin-permission-common'; +import { PermissionPolicy } from '@backstage/plugin-permission-node'; +import { PermissionIntegrationClient } from './PermissionIntegrationClient'; + +const requestSchema: z.ZodSchema[]> = z.array( + z.object({ + id: z.string(), + resourceRef: z.string().optional(), + permission: z.object({ + name: z.string(), + resourceType: z.string().optional(), + attributes: z.object({ + action: z + .union([ + z.literal('create'), + z.literal('read'), + z.literal('update'), + z.literal('delete'), + ]) + .optional(), + }), + }), + }), +); + +/** + * Options required when constructing a new {@link express#Router} using + * {@link createRouter}. + * + * @public + */ +export interface RouterOptions { + logger: Logger; + discovery: PluginEndpointDiscovery; + policy: PermissionPolicy; + identity: IdentityClient; +} + +const handleRequest = async ( + { id, resourceRef, ...request }: Identified, + user: BackstageIdentity | undefined, + policy: PermissionPolicy, + permissionIntegrationClient: PermissionIntegrationClient, + authHeader?: string, +): Promise> => { + const response = await policy.handle(request, user); + + if (response.result === AuthorizeResult.CONDITIONAL) { + // Sanity check that any resource provided matches the one expected by the permission + if (request.permission.resourceType !== response.resourceType) { + throw new Error( + `Invalid resource conditions returned from permission policy for permission ${request.permission.name}`, + ); + } + + if (resourceRef) { + return { + id, + ...(await permissionIntegrationClient.applyConditions( + { + resourceRef, + pluginId: response.pluginId, + resourceType: response.resourceType, + conditions: response.conditions, + }, + authHeader, + )), + }; + } + + return { + id, + result: AuthorizeResult.CONDITIONAL, + conditions: response.conditions, + }; + } + + return { id, ...response }; +}; + +/** + * Creates a new {@link express#Router} which provides the backend API + * for the permission system. + * + * @public + */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { policy, discovery, identity } = options; + + const permissionIntegrationClient = new PermissionIntegrationClient({ + discovery, + }); + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + response.send({ status: 'ok' }); + }); + + router.post( + '/authorize', + async ( + req: Request[]>, + res: Response[]>, + ) => { + const token = IdentityClient.getBearerToken(req.header('authorization')); + const user = token ? await identity.authenticate(token) : undefined; + + const body = requestSchema.parse(req.body); + + res.json( + await Promise.all( + body.map(request => + handleRequest( + request, + user, + policy, + permissionIntegrationClient, + req.header('authorization'), + ), + ), + ), + ); + }, + ); + + router.use(errorHandler()); + return router; +} diff --git a/plugins/permission-backend/src/setupTests.ts b/plugins/permission-backend/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/permission-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 {}; diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md new file mode 100644 index 0000000000..c2ee5a3ecf --- /dev/null +++ b/plugins/permission-common/CHANGELOG.md @@ -0,0 +1,12 @@ +# @backstage/plugin-permission-common + +## 0.2.0 + +### Minor Changes + +- 92439056fb: Accept configApi rather than enabled flag in PermissionClient constructor. + +### Patch Changes + +- Updated dependencies + - @backstage/errors@0.1.5 diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 9e1d60f782..725aef0dec 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-common", "description": "Isomorphic types and client for Backstage permissions and authorization", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -39,13 +39,13 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/errors": "^0.1.2", + "@backstage/errors": "^0.1.5", "cross-fetch": "^3.0.6", "uuid": "^8.0.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/jest": "^26.0.7", "msw": "^0.35.0" } diff --git a/plugins/permission-node/.eslintrc.js b/plugins/permission-node/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/permission-node/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md new file mode 100644 index 0000000000..37c03b7050 --- /dev/null +++ b/plugins/permission-node/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-permission-node + +## 0.1.0 + +### Minor Changes + +- 44b46644d9: New package containing common permission and authorization utilities for backend plugins. For more information, see the [authorization PRFC](https://github.com/backstage/backstage/pull/7761). + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.4.9 + - @backstage/plugin-permission-common@0.2.0 diff --git a/plugins/permission-node/README.md b/plugins/permission-node/README.md new file mode 100644 index 0000000000..82dfe54f9c --- /dev/null +++ b/plugins/permission-node/README.md @@ -0,0 +1,7 @@ +# @backstage/plugin-permission-node + +> NOTE: THIS PACKAGE IS EXPERIMENTAL, HERE BE DRAGONS + +Common permission and authorization utilities for backend plugins. For more +information, see the [authorization +PRFC](https://github.com/backstage/backstage/pull/7761). diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md new file mode 100644 index 0000000000..75c45a9250 --- /dev/null +++ b/plugins/permission-node/api-report.md @@ -0,0 +1,126 @@ +## API Report File for "@backstage/plugin-permission-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AuthorizeRequest } from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { PermissionCondition } from '@backstage/plugin-permission-common'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { Router } from 'express'; + +// @public +export type ApplyConditionsRequest = { + resourceRef: string; + resourceType: string; + conditions: PermissionCriteria; +}; + +// @public +export type ApplyConditionsResponse = { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; +}; + +// @public +export type Condition = TRule extends PermissionRule< + any, + any, + infer TParams +> + ? (...params: TParams) => PermissionCondition + : never; + +// @public +export type ConditionalPolicyDecision = { + result: AuthorizeResult.CONDITIONAL; + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; +}; + +// @public +export type Conditions< + TRules extends Record>, +> = { + [Name in keyof TRules]: Condition; +}; + +// @public +export type ConditionTransformer = ( + conditions: PermissionCriteria, +) => PermissionCriteria; + +// @public +export const createConditionExports: < + TResource, + TRules extends Record>, +>(options: { + pluginId: string; + resourceType: string; + rules: TRules; +}) => { + conditions: Conditions; + createPolicyDecision: ( + conditions: PermissionCriteria, + ) => ConditionalPolicyDecision; +}; + +// @public +export const createConditionFactory: ( + rule: PermissionRule, +) => (...params: TParams) => { + rule: string; + params: TParams; +}; + +// @public +export const createConditionTransformer: < + TQuery, + TRules extends PermissionRule[], +>( + permissionRules: [...TRules], +) => ConditionTransformer; + +// @public +export const createPermissionIntegrationRouter: ({ + resourceType, + rules, + getResource, +}: { + resourceType: string; + rules: PermissionRule[]; + getResource: (resourceRef: string) => Promise; +}) => Router; + +// @public +export interface PermissionPolicy { + // (undocumented) + handle( + request: PolicyAuthorizeRequest, + user?: BackstageIdentity, + ): Promise; +} + +// @public +export type PermissionRule< + TResource, + TQuery, + TParams extends unknown[] = unknown[], +> = { + name: string; + description: string; + apply(resource: TResource, ...params: TParams): boolean; + toQuery(...params: TParams): PermissionCriteria; +}; + +// @public +export type PolicyAuthorizeRequest = Omit; + +// @public +export type PolicyDecision = + | { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; + } + | ConditionalPolicyDecision; +``` diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json new file mode 100644 index 0000000000..2bddc21aca --- /dev/null +++ b/plugins/permission-node/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-permission-node", + "description": "Common permission and authorization utilities for backend plugins", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/permission-node" + }, + "keywords": [ + "backstage", + "permissions" + ], + "scripts": { + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/plugin-auth-backend": "^0.4.9", + "@backstage/plugin-permission-common": "^0.2.0", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "zod": "^3.11.6" + }, + "devDependencies": { + "@backstage/cli": "^0.9.1", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/permission-node/src/index.ts b/plugins/permission-node/src/index.ts new file mode 100644 index 0000000000..39527bac71 --- /dev/null +++ b/plugins/permission-node/src/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Common permission and authorization utilities for backend plugins + * + * @packageDocumentation + */ +export * from './integration'; +export * from './policy'; +export * from './types'; diff --git a/plugins/permission-node/src/integration/createConditionExports.test.ts b/plugins/permission-node/src/integration/createConditionExports.test.ts new file mode 100644 index 0000000000..f3585d11ad --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionExports.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { createConditionExports } from './createConditionExports'; + +const testIntegration = () => + createConditionExports({ + pluginId: 'test-plugin', + resourceType: 'test-resource', + rules: { + testRule1: { + name: 'testRule1', + description: 'Test rule 1', + apply: jest.fn( + (_resource: any, _firstParam: string, _secondParam: number) => true, + ), + toQuery: jest.fn((firstParam: string, secondParam: number) => ({ + query: 'testRule1', + params: [firstParam, secondParam], + })), + }, + testRule2: { + name: 'testRule2', + description: 'Test rule 2', + apply: jest.fn((_firstParam: object) => false), + toQuery: jest.fn((firstParam: object) => ({ + query: 'testRule2', + params: [firstParam], + })), + }, + }, + }); + +describe('createConditionExports', () => { + describe('conditions', () => { + it('creates condition factories for the supplied rules', () => { + const { conditions } = testIntegration(); + + expect(conditions.testRule1('a', 1)).toEqual({ + rule: 'testRule1', + params: ['a', 1], + }); + + expect(conditions.testRule2({ baz: 'quux' })).toEqual({ + rule: 'testRule2', + params: [{ baz: 'quux' }], + }); + }); + }); + + describe('createPolicyDecisions', () => { + it('wraps conditions in an object with resourceType and pluginId', () => { + const { createPolicyDecision } = testIntegration(); + + expect( + createPolicyDecision({ + allOf: [{ rule: 'testRule1', params: ['a', 1] }], + }), + ).toEqual({ + result: AuthorizeResult.CONDITIONAL, + pluginId: 'test-plugin', + resourceType: 'test-resource', + conditions: { + allOf: [{ rule: 'testRule1', params: ['a', 1] }], + }, + }); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/createConditionExports.ts b/plugins/permission-node/src/integration/createConditionExports.ts new file mode 100644 index 0000000000..fd351128ed --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionExports.ts @@ -0,0 +1,101 @@ +/* + * 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 { + AuthorizeResult, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { ConditionalPolicyDecision } from '../policy'; +import { PermissionRule } from '../types'; +import { createConditionFactory } from './createConditionFactory'; + +/** + * A utility type for mapping a single {@link PermissionRule} to its + * corresponding {@link @backstage/plugin-permission-common#PermissionCondition}. + * + * @public + */ +export type Condition = TRule extends PermissionRule< + any, + any, + infer TParams +> + ? (...params: TParams) => PermissionCondition + : never; + +/** + * A utility type for mapping {@link PermissionRule}s to their corresponding + * {@link @backstage/plugin-permission-common#PermissionCondition}s. + * + * @public + */ +export type Conditions< + TRules extends Record>, +> = { + [Name in keyof TRules]: Condition; +}; + +/** + * Creates the recommended condition-related exports for a given plugin based on the built-in + * {@link PermissionRule}s it supports. + * + * @remarks + * + * The function returns a `conditions` object containing a + * {@link @backstage/plugin-permission-common#PermissionCondition} factory for each of the + * supplied {@link PermissionRule}s, along with a `createConditions` function which builds the + * wrapper object needed to enclose conditions when authoring {@link PermissionPolicy} implementations. + * + * Plugin authors should generally call this method with all the built-in {@link PermissionRule}s + * the plugin supports, and export the resulting `conditions` object and `createConditions` + * function so that they can be used by {@link PermissionPolicy} authors. + * + * @public + */ +export const createConditionExports = < + TResource, + TRules extends Record>, +>(options: { + pluginId: string; + resourceType: string; + rules: TRules; +}): { + conditions: Conditions; + createPolicyDecision: ( + conditions: PermissionCriteria, + ) => ConditionalPolicyDecision; +} => { + const { pluginId, resourceType, rules } = options; + + return { + conditions: Object.entries(rules).reduce( + (acc, [key, rule]) => ({ + ...acc, + [key]: createConditionFactory(rule), + }), + {} as Conditions, + ), + createPolicyDecision: ( + conditions: PermissionCriteria, + ) => ({ + result: AuthorizeResult.CONDITIONAL, + pluginId, + resourceType, + conditions, + }), + }; +}; diff --git a/plugins/permission-node/src/integration/createConditionFactory.test.ts b/plugins/permission-node/src/integration/createConditionFactory.test.ts new file mode 100644 index 0000000000..8dd43f5da6 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionFactory.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { createConditionFactory } from './createConditionFactory'; + +describe('createConditionFactory', () => { + const testRule = { + name: 'test-rule', + description: 'test-description', + apply: jest.fn(), + toQuery: jest.fn(), + }; + + it('returns a function', () => { + expect(createConditionFactory(testRule)).toEqual(expect.any(Function)); + }); + + describe('return value', () => { + it('constructs a condition with the rule name and supplied params', () => { + const conditionFactory = createConditionFactory(testRule); + expect(conditionFactory('a', 'b', 1, 2)).toEqual({ + rule: 'test-rule', + params: ['a', 'b', 1, 2], + }); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/createConditionFactory.ts b/plugins/permission-node/src/integration/createConditionFactory.ts new file mode 100644 index 0000000000..84a8dc86a6 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionFactory.ts @@ -0,0 +1,40 @@ +/* + * 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 { PermissionRule } from '../types'; + +/** + * Creates a condition factory function for a given authorization rule and parameter types. + * + * @remarks + * + * For example, an isEntityOwner rule for catalog entities might take an array of entityRef strings. + * The rule itself defines _how_ to check a given resource, whereas a condition also includes _what_ + * to verify. + * + * Plugin authors should generally use the {@link createConditionExports} in order to efficiently + * create multiple condition factories. This helper should generally only be used to construct + * condition factories for third-party rules that aren't part of the backend plugin with which + * they're intended to integrate. + * + * @public + */ +export const createConditionFactory = + (rule: PermissionRule) => + (...params: TParams) => ({ + rule: rule.name, + params, + }); diff --git a/plugins/permission-node/src/integration/createConditionTransformer.test.ts b/plugins/permission-node/src/integration/createConditionTransformer.test.ts new file mode 100644 index 0000000000..8a93a4ea27 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionTransformer.test.ts @@ -0,0 +1,158 @@ +/* + * 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 { + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { createConditionTransformer } from './createConditionTransformer'; + +const transformConditions = createConditionTransformer([ + { + name: 'test-rule-1', + description: 'Test rule 1', + apply: jest.fn(), + toQuery: jest.fn( + (firstParam: string, secondParam: number) => + `test-rule-1:${firstParam}/${secondParam}`, + ), + }, + { + name: 'test-rule-2', + description: 'Test rule 2', + apply: jest.fn(), + toQuery: jest.fn( + (firstParam: object) => `test-rule-2:${JSON.stringify(firstParam)}`, + ), + }, +]); + +describe('createConditionTransformer', () => { + const testCases: { + conditions: PermissionCriteria; + expectedResult: PermissionCriteria; + }[] = [ + { + conditions: { rule: 'test-rule-1', params: ['abc', 123] }, + expectedResult: 'test-rule-1:abc/123', + }, + { + conditions: { rule: 'test-rule-2', params: [{ foo: 0 }] }, + expectedResult: 'test-rule-2:{"foo":0}', + }, + { + conditions: { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + expectedResult: { + anyOf: ['test-rule-1:a/1', 'test-rule-2:{}'], + }, + }, + { + conditions: { + allOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + expectedResult: { + allOf: ['test-rule-1:a/1', 'test-rule-2:{}'], + }, + }, + { + conditions: { + not: { rule: 'test-rule-2', params: [{}] }, + }, + expectedResult: { + not: 'test-rule-2:{}', + }, + }, + { + conditions: { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['b', 2] }, + { rule: 'test-rule-2', params: [{ c: 3 }] }, + ], + }, + }, + ], + }, + expectedResult: { + allOf: [ + { + anyOf: ['test-rule-1:a/1', 'test-rule-2:{}'], + }, + { + not: { + allOf: ['test-rule-1:b/2', 'test-rule-2:{"c":3}'], + }, + }, + ], + }, + }, + { + conditions: { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{ b: 2 }] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['c', 3] }, + { not: { rule: 'test-rule-2', params: [{ d: 4 }] } }, + ], + }, + }, + ], + }, + expectedResult: { + allOf: [ + { + anyOf: ['test-rule-1:a/1', 'test-rule-2:{"b":2}'], + }, + { + not: { + allOf: ['test-rule-1:c/3', { not: 'test-rule-2:{"d":4}' }], + }, + }, + ], + }, + }, + ]; + + it.each(testCases)( + 'works with criteria %#', + ({ conditions, expectedResult }) => { + expect(transformConditions(conditions)).toEqual(expectedResult); + }, + ); +}); diff --git a/plugins/permission-node/src/integration/createConditionTransformer.ts b/plugins/permission-node/src/integration/createConditionTransformer.ts new file mode 100644 index 0000000000..0e736b832c --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionTransformer.ts @@ -0,0 +1,78 @@ +/* + * 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 { + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; +import { + createGetRule, + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from './util'; + +const mapConditions = ( + criteria: PermissionCriteria, + getRule: (name: string) => PermissionRule, +): PermissionCriteria => { + if (isAndCriteria(criteria)) { + return { + allOf: criteria.allOf.map(child => mapConditions(child, getRule)), + }; + } else if (isOrCriteria(criteria)) { + return { + anyOf: criteria.anyOf.map(child => mapConditions(child, getRule)), + }; + } else if (isNotCriteria(criteria)) { + return { + not: mapConditions(criteria.not, getRule), + }; + } + + return getRule(criteria.rule).toQuery(...criteria.params); +}; + +/** + * A function which accepts {@link @backstage/plugin-permission-common#PermissionCondition}s + * logically grouped in a {@link @backstage/plugin-permission-common#PermissionCriteria} + * object, and transforms the {@link @backstage/plugin-permission-common#PermissionCondition}s + * into plugin specific query fragments while retaining the enclosing criteria shape. + * + * @public + */ +export type ConditionTransformer = ( + conditions: PermissionCriteria, +) => PermissionCriteria; + +/** + * A higher-order helper function which accepts an array of + * {@link PermissionRule}s, and returns a {@link ConditionTransformer} + * which transforms input conditions into equivalent plugin-specific + * query fragments using the supplied rules. + * + * @public + */ +export const createConditionTransformer = < + TQuery, + TRules extends PermissionRule[], +>( + permissionRules: [...TRules], +): ConditionTransformer => { + const getRule = createGetRule(permissionRules); + + return conditions => mapConditions(conditions, getRule); +}; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts new file mode 100644 index 0000000000..af216dfcb8 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -0,0 +1,210 @@ +/* + * 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 { AuthorizeResult } from '@backstage/plugin-permission-common'; +import express, { Express, Router } from 'express'; +import request from 'supertest'; +import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter'; + +const mockGetResource: jest.MockedFunction< + (resourceRef: string) => Promise +> = jest.fn((resourceRef: string) => + Promise.resolve({ + resourceRef, + }), +); + +const testRule1 = { + name: 'test-rule-1', + description: 'Test rule 1', + apply: jest.fn( + (_resource: any, _firstParam: string, _secondParam: number) => true, + ), + toQuery: jest.fn(), +}; + +const testRule2 = { + name: 'test-rule-2', + description: 'Test rule 2', + apply: jest.fn((_firstParam: object) => false), + toQuery: jest.fn(), +}; + +describe('createPermissionIntegrationRouter', () => { + let app: Express; + let router: Router; + + beforeEach(() => { + router = createPermissionIntegrationRouter({ + resourceType: 'test-resource', + getResource: mockGetResource, + rules: [testRule1, testRule2], + }); + + app = express().use(router); + }); + + it('works', async () => { + expect(router).toBeDefined(); + }); + + describe('POST /permissions/apply-conditions', () => { + it.each([ + { rule: 'test-rule-1', params: ['abc', 123] }, + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + + { + not: { rule: 'test-rule-2', params: [{}] }, + }, + { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['b', 2] }, + { rule: 'test-rule-2', params: [{ c: 3 }] }, + ], + }, + }, + ], + }, + ])('returns 200/ALLOW when criteria match (case %#)', async conditions => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ result: AuthorizeResult.ALLOW }); + }); + + it.each([ + { rule: 'test-rule-2', params: [{ foo: 0 }] }, + { + allOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{ b: 2 }] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['c', 3] }, + { not: { rule: 'test-rule-2', params: [{ d: 4 }] } }, + ], + }, + }, + ], + }, + ])( + 'returns 200/DENY when criteria do not match (case %#)', + async conditions => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ result: AuthorizeResult.DENY }); + }, + ); + + it('returns 400 when called with incorrect resource type', async () => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-incorrect-resource', + conditions: { + anyOf: [], + }, + }); + + expect(response.status).toEqual(400); + expect(response.error && response.error.text).toMatch( + /unexpected resource type: test-incorrect-resource/i, + ); + }); + + it('returns 400 when resource is not found', async () => { + mockGetResource.mockReturnValueOnce(Promise.resolve(undefined)); + + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions: { + not: { + rule: 'testRule1', + params: ['a', 1], + }, + }, + }); + + expect(response.status).toEqual(400); + expect(response.error && response.error.text).toMatch( + /resource for ref default:test\/resource not found/i, + ); + }); + + it.each([ + undefined, + {}, + { resourceType: 'test-resource-type' }, + { resourceRef: 'test/resource-ref' }, + { + resourceType: 'test-resource-type', + resourceRef: 'test/resource-ref', + }, + { conditions: { anyOf: [] } }, + ])(`returns 400 for invalid input %#`, async input => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send(input); + + expect(response.status).toEqual(400); + expect(response.error && response.error.text).toMatch( + /invalid request body/i, + ); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts new file mode 100644 index 0000000000..99e282b144 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -0,0 +1,177 @@ +/* + * 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 express, { Response, Router } from 'express'; +import { z } from 'zod'; +import { + AuthorizeResult, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; +import { + createGetRule, + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from './util'; + +const permissionCriteriaSchema: z.ZodSchema< + PermissionCriteria +> = z.lazy(() => + z.union([ + z.object({ anyOf: z.array(permissionCriteriaSchema) }), + z.object({ allOf: z.array(permissionCriteriaSchema) }), + z.object({ not: permissionCriteriaSchema }), + z.object({ + rule: z.string(), + params: z.array(z.unknown()), + }), + ]), +); + +const applyConditionsRequestSchema = z.object({ + resourceRef: z.string(), + resourceType: z.string(), + conditions: permissionCriteriaSchema, +}); + +/** + * A request to load the referenced resource and apply conditions in order to + * finalize a conditional authorization response. + * + * @public + */ +export type ApplyConditionsRequest = { + resourceRef: string; + resourceType: string; + conditions: PermissionCriteria; +}; + +/** + * The result of applying the conditions, expressed as a definitive authorize + * result of ALLOW or DENY. + * + * @public + */ +export type ApplyConditionsResponse = { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; +}; + +const applyConditions = ( + criteria: PermissionCriteria, + resource: TResource, + getRule: (name: string) => PermissionRule, +): boolean => { + if (isAndCriteria(criteria)) { + return criteria.allOf.every(child => + applyConditions(child, resource, getRule), + ); + } else if (isOrCriteria(criteria)) { + return criteria.anyOf.some(child => + applyConditions(child, resource, getRule), + ); + } else if (isNotCriteria(criteria)) { + return !applyConditions(criteria.not, resource, getRule); + } + + return getRule(criteria.rule).apply(resource, ...criteria.params); +}; + +/** + * Create an express Router which provides an authorization route to allow integration between the + * permission backend and other Backstage backend plugins. Plugin owners that wish to support + * conditional authorization for their resources should add the router created by this function + * to their express app inside their `createRouter` implementation. + * + * @remarks + * + * To make this concrete, we can use the Backstage software catalog as an example. The catalog has + * conditional rules around access to specific _entities_ in the catalog. The _type_ of resource is + * captured here as `resourceType`, a string identifier (`catalog-entity` in this example) that can + * be provided with permission definitions. This is merely a _type_ to verify that conditions in an + * authorization policy are constructed correctly, not a reference to a specific resource. + * + * The `rules` parameter is an array of {@link PermissionRule}s that introduce conditional + * filtering logic for resources; for the catalog, these are things like `isEntityOwner` or + * `hasAnnotation`. Rules describe how to filter a list of resources, and the `conditions` returned + * allow these rules to be applied with specific parameters (such as 'group:default/team-a', or + * 'backstage.io/edit-url'). + * + * The `getResource` argument should load a resource by reference. For the catalog, this is an + * {@link @backstage/catalog-model#EntityRef}. For other plugins, this can be any serialized format. + * This is used to construct the `createPermissionIntegrationRouter`, a function to add an + * authorization route to your backend plugin. This route will be called by the `permission-backend` + * when authorization conditions relating to this plugin need to be evaluated. + * @public + */ +export const createPermissionIntegrationRouter = ({ + resourceType, + rules, + getResource, +}: { + resourceType: string; + rules: PermissionRule[]; + getResource: (resourceRef: string) => Promise; +}): Router => { + const router = Router(); + + const getRule = createGetRule(rules); + + router.post( + '/permissions/apply-conditions', + express.json(), + async ( + req, + res: Response< + | { + result: Omit; + } + | string + >, + ) => { + const parseResult = applyConditionsRequestSchema.safeParse(req.body); + + if (!parseResult.success) { + return res.status(400).send(`Invalid request body.`); + } + + const { data: body } = parseResult; + + if (body.resourceType !== resourceType) { + return res + .status(400) + .send(`Unexpected resource type: ${body.resourceType}.`); + } + + const resource = await getResource(body.resourceRef); + + if (!resource) { + return res + .status(400) + .send(`Resource for ref ${body.resourceRef} not found.`); + } + + return res.status(200).json({ + result: applyConditions(body.conditions, resource, getRule) + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + }); + }, + ); + + return router; +}; diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts new file mode 100644 index 0000000000..f070d57c8f --- /dev/null +++ b/plugins/permission-node/src/integration/index.ts @@ -0,0 +1,20 @@ +/* + * 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 * from './createConditionFactory'; +export * from './createConditionExports'; +export * from './createConditionTransformer'; +export * from './createPermissionIntegrationRouter'; diff --git a/plugins/permission-node/src/integration/util.test.ts b/plugins/permission-node/src/integration/util.test.ts new file mode 100644 index 0000000000..d27c0a4243 --- /dev/null +++ b/plugins/permission-node/src/integration/util.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { + createGetRule, + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from './util'; + +describe('permission integration utils', () => { + describe('createGetRule', () => { + let getRule: ReturnType; + + const testRule1 = { + name: 'test-rule-1', + description: 'Test rule 1', + apply: jest.fn(), + toQuery: jest.fn(), + }; + + const testRule2 = { + name: 'test-rule-2', + description: 'Test rule 2', + apply: jest.fn(), + toQuery: jest.fn(), + }; + + beforeEach(() => { + getRule = createGetRule([testRule1, testRule2]); + }); + + it('returns the rule matching the supplied name', () => { + expect(getRule('test-rule-1')).toBe(testRule1); + }); + + it('throws if there is no rule for the supplied name', () => { + expect(() => getRule('test-rule-3')).toThrowError( + /unexpected permission rule/i, + ); + }); + }); + + describe('isOrCriteria', () => { + it('returns true if input has a top-level "anyOf" property', () => { + expect(isOrCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(true); + }); + + it('returns false if input does not have a top-level "anyOf" property', () => { + expect(isOrCriteria({ allOf: { not: { anyOf: [] } } })).toEqual(false); + }); + }); + + describe('isAndCriteria', () => { + it('returns true if input has a top-level "allOf" property', () => { + expect(isAndCriteria({ allOf: { not: { anyOf: [] } } })).toEqual(true); + }); + + it('returns false if input does not have a top-level "allOf" property', () => { + expect(isAndCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(false); + }); + }); + + describe('isNotCriteria', () => { + it('returns true if input has a top-level "not" property', () => { + expect(isNotCriteria({ not: { allOf: [{ anyOf: [] }] } })).toEqual(true); + }); + + it('returns false if input does not have a top-level "not" property', () => { + expect(isNotCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(false); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts new file mode 100644 index 0000000000..d9457cfefc --- /dev/null +++ b/plugins/permission-node/src/integration/util.ts @@ -0,0 +1,49 @@ +/* + * 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 { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; + +export const isAndCriteria = ( + filter: PermissionCriteria, +): filter is { allOf: PermissionCriteria[] } => + Object.prototype.hasOwnProperty.call(filter, 'allOf'); + +export const isOrCriteria = ( + filter: PermissionCriteria, +): filter is { anyOf: PermissionCriteria[] } => + Object.prototype.hasOwnProperty.call(filter, 'anyOf'); + +export const isNotCriteria = ( + filter: PermissionCriteria, +): filter is { not: PermissionCriteria } => + Object.prototype.hasOwnProperty.call(filter, 'not'); + +export const createGetRule = ( + rules: PermissionRule[], +) => { + const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule])); + + return (name: string): PermissionRule => { + const rule = rulesMap.get(name); + + if (!rule) { + throw new Error(`Unexpected permission rule: ${name}`); + } + + return rule; + }; +}; diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts new file mode 100644 index 0000000000..c8216989a1 --- /dev/null +++ b/plugins/permission-node/src/policy/index.ts @@ -0,0 +1,22 @@ +/* + * 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 type { + ConditionalPolicyDecision, + PermissionPolicy, + PolicyAuthorizeRequest, + PolicyDecision, +} from './types'; diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts new file mode 100644 index 0000000000..3548d051f6 --- /dev/null +++ b/plugins/permission-node/src/policy/types.ts @@ -0,0 +1,88 @@ +/* + * 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 { + AuthorizeRequest, + AuthorizeResult, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { BackstageIdentity } from '@backstage/plugin-auth-backend'; + +/** + * An authorization request to be evaluated by the {@link PermissionPolicy}. + * + * @remarks + * + * This differs from {@link @backstage/permission-common#AuthorizeRequest} in that `resourceRef` + * should never be provided. This forces policies to be written in a way that's compatible with + * filtering collections of resources at data load time. + * + * @public + */ +export type PolicyAuthorizeRequest = Omit; + +/** + * A conditional result to an authorization request, returned by the {@link PermissionPolicy}. + * + * @remarks + * + * This indicates that the policy allows authorization for the request, given that the returned + * conditions hold when evaluated. The conditions will be evaluated by the corresponding plugin + * which knows about the referenced permission rules. + * + * Similar to {@link @backstage/permission-common#AuthorizeResult}, but with the plugin and resource + * identifiers needed to evaluate the returned conditions. + * @public + */ +export type ConditionalPolicyDecision = { + result: AuthorizeResult.CONDITIONAL; + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; +}; + +/** + * The result of evaluating an authorization request with a {@link PermissionPolicy}. + * + * @public + */ +export type PolicyDecision = + | { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY } + | ConditionalPolicyDecision; + +/** + * A policy to evaluate authorization requests for any permissioned action performed in Backstage. + * + * @remarks + * + * This takes as input a permission and an optional Backstage identity, and should return ALLOW if + * the user is permitted to execute that action; otherwise DENY. For permissions relating to + * resources, such a catalog entities, a conditional response can also be returned. This states + * that the action is allowed if the conditions provided hold true. + * + * Conditions are a rule, and parameters to evaluate against that rule. For example, the rule might + * be `isOwner` and the parameters a collection of entityRefs; if one of the entityRefs matches + * the `owner` field on a catalog entity, this would resolve to ALLOW. + * + * @public + */ +export interface PermissionPolicy { + handle( + request: PolicyAuthorizeRequest, + user?: BackstageIdentity, + ): Promise; +} diff --git a/plugins/permission-node/src/setupTests.ts b/plugins/permission-node/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/permission-node/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 {}; diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts new file mode 100644 index 0000000000..678befc99c --- /dev/null +++ b/plugins/permission-node/src/types.ts @@ -0,0 +1,56 @@ +/* + * 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 type { PermissionCriteria } from '@backstage/plugin-permission-common'; + +/** + * A conditional rule that can be provided in an + * {@link @backstage/permission-common#AuthorizeResult} response to an authorization request. + * + * @remarks + * + * Rules can either be evaluated against a resource loaded in memory, or used as filters when + * loading a collection of resources from a data source. The `apply` and `toQuery` methods implement + * these two concepts. + * + * The two operations should always have the same logical result. If they don’t, the effective + * outcome of an authorization operation will sometimes differ depending on how the authorization + * check was performed. + * + * @public + */ +export type PermissionRule< + TResource, + TQuery, + TParams extends unknown[] = unknown[], +> = { + name: string; + description: string; + + /** + * Apply this rule to a resource already loaded from a backing data source. The params are + * arguments supplied for the rule; for example, a rule could be `isOwner` with entityRefs as the + * params. + */ + apply(resource: TResource, ...params: TParams): boolean; + + /** + * Translate this rule to criteria suitable for use in querying a backing data store. The criteria + * can be used for loading a collection of resources efficiently with conditional criteria already + * applied. + */ + toQuery(...params: TParams): PermissionCriteria; +}; diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index a7408c161f..57b3441fa5 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-proxy-backend +## 0.2.14 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/backend-common@0.9.11 + ## 0.2.13 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 05499d88e8..78586449c7 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.13", + "version": "0.2.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/config": "^0.1.8", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -43,7 +43,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index d2bc07ab7e..9c0f70ea6a 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-rollbar-backend +## 0.1.16 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/backend-common@0.9.11 + ## 0.1.15 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index be8b6e65e5..bb30692476 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.15", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", "axios": "^0.21.1", @@ -48,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index ee3fc9f5a6..0e4086bd4a 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -49,10 +49,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 90258d3c28..95dab84e88 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -29,7 +29,6 @@ "command-exists": "^1.2.9", "fs-extra": "10.0.0", "winston": "^3.2.1", - "cross-fetch": "^3.0.6", "yn": "^4.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend/.eslintrc.js b/plugins/scaffolder-backend/.eslintrc.js index 19c9ad7395..2632b5e089 100644 --- a/plugins/scaffolder-backend/.eslintrc.js +++ b/plugins/scaffolder-backend/.eslintrc.js @@ -1,8 +1,41 @@ +const parent = require('@backstage/cli/config/eslint.backend'); + module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], ignorePatterns: ['sample-templates/'], rules: { 'no-console': 0, // Permitted in console programs 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' + // Usage of path.resolve is extra sensitive in the scaffolder, so forbid it in non-test code + 'no-restricted-imports': [ + 'error', + { + ...parent.rules['no-restricted-imports'][1], + paths: [ + { + name: 'path', + importNames: ['resolve'], + message: + 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', + }, + ], + }, + ], + 'no-restricted-syntax': parent.rules['no-restricted-syntax'].concat([ + { + message: + 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', + selector: + 'MemberExpression[object.name="path"][property.name="resolve"]', + }, + ]), }, + overrides: [ + { + files: ['*.test.*', 'src/setupTests.*', 'dev/**'], + rules: { + 'no-restricted-imports': parent.rules['no-restricted-imports'], + }, + }, + ], }; diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index f71849a1d2..092a678fce 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend +## 0.15.14 + +### Patch Changes + +- a096e4c4d7: Switched to executing scaffolder templating in a secure context for any template based on nunjucks, as it is [not secure by default](https://mozilla.github.io/nunjucks/api.html#user-defined-templates-warning). +- f9352ab606: Removed all usages of `path.resolve` in order to ensure that template paths are resolved in a safe way. +- e634a47ce5: Fix bug where there was error log lines written when failing to `JSON.parse` things that were not `JSON` values. +- 42ebbc18c0: Bump gitbeaker to the latest version +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/plugin-catalog-backend@0.18.0 + - @backstage/backend-common@0.9.11 + ## 0.15.13 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 4b27b938c9..6ed4625f66 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -24,6 +24,7 @@ import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; +import { SpawnOptionsWithoutStdio } from 'child_process'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; @@ -304,11 +305,12 @@ export interface RouterOptions { // Warning: (ae-forgotten-export) The symbol "RunCommandOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "runCommand" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export const runCommand: ({ command, args, logStream, + options, }: RunCommandOptions) => Promise; // @public (undocumented) diff --git a/plugins/scaffolder-backend/assets/nunjucks.js.txt b/plugins/scaffolder-backend/assets/nunjucks.js.txt new file mode 100644 index 0000000000..b78d1d98ac --- /dev/null +++ b/plugins/scaffolder-backend/assets/nunjucks.js.txt @@ -0,0 +1,10385 @@ + +/** + * Copyright (c) 2012-2015, James Long + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var __commonJS = (callback, module2) => () => { + if (!module2) { + module2 = {exports: {}}; + callback(module2.exports, module2); + } + return module2.exports; +}; + +// ../../node_modules/nunjucks/src/lib.js +var require_lib = __commonJS((exports2, module2) => { + "use strict"; + var ArrayProto = Array.prototype; + var ObjProto = Object.prototype; + var escapeMap = { + "&": "&", + '"': """, + "'": "'", + "<": "<", + ">": ">" + }; + var escapeRegex = /[&"'<>]/g; + var _exports = module2.exports = {}; + function hasOwnProp(obj, k) { + return ObjProto.hasOwnProperty.call(obj, k); + } + _exports.hasOwnProp = hasOwnProp; + function lookupEscape(ch) { + return escapeMap[ch]; + } + function _prettifyError(path, withInternals, err) { + if (!err.Update) { + err = new _exports.TemplateError(err); + } + err.Update(path); + if (!withInternals) { + var old = err; + err = new Error(old.message); + err.name = old.name; + } + return err; + } + _exports._prettifyError = _prettifyError; + function TemplateError(message, lineno, colno) { + var err; + var cause; + if (message instanceof Error) { + cause = message; + message = cause.name + ": " + cause.message; + } + if (Object.setPrototypeOf) { + err = new Error(message); + Object.setPrototypeOf(err, TemplateError.prototype); + } else { + err = this; + Object.defineProperty(err, "message", { + enumerable: false, + writable: true, + value: message + }); + } + Object.defineProperty(err, "name", { + value: "Template render error" + }); + if (Error.captureStackTrace) { + Error.captureStackTrace(err, this.constructor); + } + var getStack; + if (cause) { + var stackDescriptor = Object.getOwnPropertyDescriptor(cause, "stack"); + getStack = stackDescriptor && (stackDescriptor.get || function() { + return stackDescriptor.value; + }); + if (!getStack) { + getStack = function getStack2() { + return cause.stack; + }; + } + } else { + var stack = new Error(message).stack; + getStack = function getStack2() { + return stack; + }; + } + Object.defineProperty(err, "stack", { + get: function get() { + return getStack.call(err); + } + }); + Object.defineProperty(err, "cause", { + value: cause + }); + err.lineno = lineno; + err.colno = colno; + err.firstUpdate = true; + err.Update = function Update(path) { + var msg = "(" + (path || "unknown path") + ")"; + if (this.firstUpdate) { + if (this.lineno && this.colno) { + msg += " [Line " + this.lineno + ", Column " + this.colno + "]"; + } else if (this.lineno) { + msg += " [Line " + this.lineno + "]"; + } + } + msg += "\n "; + if (this.firstUpdate) { + msg += " "; + } + this.message = msg + (this.message || ""); + this.firstUpdate = false; + return this; + }; + return err; + } + if (Object.setPrototypeOf) { + Object.setPrototypeOf(TemplateError.prototype, Error.prototype); + } else { + TemplateError.prototype = Object.create(Error.prototype, { + constructor: { + value: TemplateError + } + }); + } + _exports.TemplateError = TemplateError; + function escape(val) { + return val.replace(escapeRegex, lookupEscape); + } + _exports.escape = escape; + function isFunction(obj) { + return ObjProto.toString.call(obj) === "[object Function]"; + } + _exports.isFunction = isFunction; + function isArray(obj) { + return ObjProto.toString.call(obj) === "[object Array]"; + } + _exports.isArray = isArray; + function isString(obj) { + return ObjProto.toString.call(obj) === "[object String]"; + } + _exports.isString = isString; + function isObject(obj) { + return ObjProto.toString.call(obj) === "[object Object]"; + } + _exports.isObject = isObject; + function _prepareAttributeParts(attr) { + if (!attr) { + return []; + } + if (typeof attr === "string") { + return attr.split("."); + } + return [attr]; + } + function getAttrGetter(attribute) { + var parts = _prepareAttributeParts(attribute); + return function attrGetter(item) { + var _item = item; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + if (hasOwnProp(_item, part)) { + _item = _item[part]; + } else { + return void 0; + } + } + return _item; + }; + } + _exports.getAttrGetter = getAttrGetter; + function groupBy(obj, val, throwOnUndefined) { + var result = {}; + var iterator = isFunction(val) ? val : getAttrGetter(val); + for (var i = 0; i < obj.length; i++) { + var value = obj[i]; + var key = iterator(value, i); + if (key === void 0 && throwOnUndefined === true) { + throw new TypeError('groupby: attribute "' + val + '" resolved to undefined'); + } + (result[key] || (result[key] = [])).push(value); + } + return result; + } + _exports.groupBy = groupBy; + function toArray(obj) { + return Array.prototype.slice.call(obj); + } + _exports.toArray = toArray; + function without(array) { + var result = []; + if (!array) { + return result; + } + var length = array.length; + var contains = toArray(arguments).slice(1); + var index = -1; + while (++index < length) { + if (indexOf(contains, array[index]) === -1) { + result.push(array[index]); + } + } + return result; + } + _exports.without = without; + function repeat(char_, n) { + var str = ""; + for (var i = 0; i < n; i++) { + str += char_; + } + return str; + } + _exports.repeat = repeat; + function each(obj, func, context) { + if (obj == null) { + return; + } + if (ArrayProto.forEach && obj.forEach === ArrayProto.forEach) { + obj.forEach(func, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + func.call(context, obj[i], i, obj); + } + } + } + _exports.each = each; + function map(obj, func) { + var results = []; + if (obj == null) { + return results; + } + if (ArrayProto.map && obj.map === ArrayProto.map) { + return obj.map(func); + } + for (var i = 0; i < obj.length; i++) { + results[results.length] = func(obj[i], i); + } + if (obj.length === +obj.length) { + results.length = obj.length; + } + return results; + } + _exports.map = map; + function asyncIter(arr, iter, cb) { + var i = -1; + function next() { + i++; + if (i < arr.length) { + iter(arr[i], i, next, cb); + } else { + cb(); + } + } + next(); + } + _exports.asyncIter = asyncIter; + function asyncFor(obj, iter, cb) { + var keys = keys_(obj || {}); + var len = keys.length; + var i = -1; + function next() { + i++; + var k = keys[i]; + if (i < len) { + iter(k, obj[k], i, len, next); + } else { + cb(); + } + } + next(); + } + _exports.asyncFor = asyncFor; + function indexOf(arr, searchElement, fromIndex) { + return Array.prototype.indexOf.call(arr || [], searchElement, fromIndex); + } + _exports.indexOf = indexOf; + function keys_(obj) { + var arr = []; + for (var k in obj) { + if (hasOwnProp(obj, k)) { + arr.push(k); + } + } + return arr; + } + _exports.keys = keys_; + function _entries(obj) { + return keys_(obj).map(function(k) { + return [k, obj[k]]; + }); + } + _exports._entries = _entries; + function _values(obj) { + return keys_(obj).map(function(k) { + return obj[k]; + }); + } + _exports._values = _values; + function extend(obj1, obj2) { + obj1 = obj1 || {}; + keys_(obj2).forEach(function(k) { + obj1[k] = obj2[k]; + }); + return obj1; + } + _exports._assign = _exports.extend = extend; + function inOperator(key, val) { + if (isArray(val) || isString(val)) { + return val.indexOf(key) !== -1; + } else if (isObject(val)) { + return key in val; + } + throw new Error('Cannot use "in" operator to search for "' + key + '" in unexpected types.'); + } + _exports.inOperator = inOperator; +}); + +// ../../node_modules/asap/raw.js +var require_raw = __commonJS((exports2, module2) => { + "use strict"; + var domain; + var hasSetImmediate = typeof setImmediate === "function"; + module2.exports = rawAsap; + function rawAsap(task) { + if (!queue.length) { + requestFlush(); + flushing = true; + } + queue[queue.length] = task; + } + var queue = []; + var flushing = false; + var index = 0; + var capacity = 1024; + function flush() { + while (index < queue.length) { + var currentIndex = index; + index = index + 1; + queue[currentIndex].call(); + if (index > capacity) { + for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) { + queue[scan] = queue[scan + index]; + } + queue.length -= index; + index = 0; + } + } + queue.length = 0; + index = 0; + flushing = false; + } + rawAsap.requestFlush = requestFlush; + function requestFlush() { + var parentDomain = process.domain; + if (parentDomain) { + if (!domain) { + domain = require("domain"); + } + domain.active = process.domain = null; + } + if (flushing && hasSetImmediate) { + setImmediate(flush); + } else { + process.nextTick(flush); + } + if (parentDomain) { + domain.active = process.domain = parentDomain; + } + } +}); + +// ../../node_modules/asap/asap.js +var require_asap = __commonJS((exports2, module2) => { + "use strict"; + var rawAsap = require_raw(); + var freeTasks = []; + module2.exports = asap; + function asap(task) { + var rawTask; + if (freeTasks.length) { + rawTask = freeTasks.pop(); + } else { + rawTask = new RawTask(); + } + rawTask.task = task; + rawTask.domain = process.domain; + rawAsap(rawTask); + } + function RawTask() { + this.task = null; + this.domain = null; + } + RawTask.prototype.call = function() { + if (this.domain) { + this.domain.enter(); + } + var threw = true; + try { + this.task.call(); + threw = false; + if (this.domain) { + this.domain.exit(); + } + } finally { + if (threw) { + rawAsap.requestFlush(); + } + this.task = null; + this.domain = null; + freeTasks.push(this); + } + }; +}); + +// ../../node_modules/a-sync-waterfall/index.js +var require_a_sync_waterfall = __commonJS((exports2, module2) => { + (function(globals) { + "use strict"; + var executeSync = function() { + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === "function") { + args[0].apply(null, args.splice(1)); + } + }; + var executeAsync = function(fn) { + if (typeof setImmediate === "function") { + setImmediate(fn); + } else if (typeof process !== "undefined" && process.nextTick) { + process.nextTick(fn); + } else { + setTimeout(fn, 0); + } + }; + var makeIterator = function(tasks) { + var makeCallback = function(index) { + var fn = function() { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function() { + return index < tasks.length - 1 ? makeCallback(index + 1) : null; + }; + return fn; + }; + return makeCallback(0); + }; + var _isArray = Array.isArray || function(maybeArray) { + return Object.prototype.toString.call(maybeArray) === "[object Array]"; + }; + var waterfall = function(tasks, callback, forceAsync) { + var nextTick = forceAsync ? executeAsync : executeSync; + callback = callback || function() { + }; + if (!_isArray(tasks)) { + var err = new Error("First argument to waterfall must be an array of functions"); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function(iterator) { + return function(err2) { + if (err2) { + callback.apply(null, arguments); + callback = function() { + }; + } else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } else { + args.push(callback); + } + nextTick(function() { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(makeIterator(tasks))(); + }; + if (typeof define !== "undefined" && define.amd) { + define([], function() { + return waterfall; + }); + } else if (typeof module2 !== "undefined" && module2.exports) { + module2.exports = waterfall; + } else { + globals.waterfall = waterfall; + } + })(exports2); +}); + +// ../../node_modules/nunjucks/src/lexer.js +var require_lexer = __commonJS((exports2, module2) => { + "use strict"; + var lib2 = require_lib(); + var whitespaceChars = " \n \r\xA0"; + var delimChars = "()[]{}%*-+~/#,:|.<>=!"; + var intChars = "0123456789"; + var BLOCK_START = "{%"; + var BLOCK_END = "%}"; + var VARIABLE_START = "{{"; + var VARIABLE_END = "}}"; + var COMMENT_START = "{#"; + var COMMENT_END = "#}"; + var TOKEN_STRING = "string"; + var TOKEN_WHITESPACE = "whitespace"; + var TOKEN_DATA = "data"; + var TOKEN_BLOCK_START = "block-start"; + var TOKEN_BLOCK_END = "block-end"; + var TOKEN_VARIABLE_START = "variable-start"; + var TOKEN_VARIABLE_END = "variable-end"; + var TOKEN_COMMENT = "comment"; + var TOKEN_LEFT_PAREN = "left-paren"; + var TOKEN_RIGHT_PAREN = "right-paren"; + var TOKEN_LEFT_BRACKET = "left-bracket"; + var TOKEN_RIGHT_BRACKET = "right-bracket"; + var TOKEN_LEFT_CURLY = "left-curly"; + var TOKEN_RIGHT_CURLY = "right-curly"; + var TOKEN_OPERATOR = "operator"; + var TOKEN_COMMA = "comma"; + var TOKEN_COLON = "colon"; + var TOKEN_TILDE = "tilde"; + var TOKEN_PIPE = "pipe"; + var TOKEN_INT = "int"; + var TOKEN_FLOAT = "float"; + var TOKEN_BOOLEAN = "boolean"; + var TOKEN_NONE = "none"; + var TOKEN_SYMBOL = "symbol"; + var TOKEN_SPECIAL = "special"; + var TOKEN_REGEX = "regex"; + function token(type, value, lineno, colno) { + return { + type, + value, + lineno, + colno + }; + } + var Tokenizer = /* @__PURE__ */ function() { + function Tokenizer2(str, opts) { + this.str = str; + this.index = 0; + this.len = str.length; + this.lineno = 0; + this.colno = 0; + this.in_code = false; + opts = opts || {}; + var tags = opts.tags || {}; + this.tags = { + BLOCK_START: tags.blockStart || BLOCK_START, + BLOCK_END: tags.blockEnd || BLOCK_END, + VARIABLE_START: tags.variableStart || VARIABLE_START, + VARIABLE_END: tags.variableEnd || VARIABLE_END, + COMMENT_START: tags.commentStart || COMMENT_START, + COMMENT_END: tags.commentEnd || COMMENT_END + }; + this.trimBlocks = !!opts.trimBlocks; + this.lstripBlocks = !!opts.lstripBlocks; + } + var _proto = Tokenizer2.prototype; + _proto.nextToken = function nextToken() { + var lineno = this.lineno; + var colno = this.colno; + var tok; + if (this.in_code) { + var cur = this.current(); + if (this.isFinished()) { + return null; + } else if (cur === '"' || cur === "'") { + return token(TOKEN_STRING, this._parseString(cur), lineno, colno); + } else if (tok = this._extract(whitespaceChars)) { + return token(TOKEN_WHITESPACE, tok, lineno, colno); + } else if ((tok = this._extractString(this.tags.BLOCK_END)) || (tok = this._extractString("-" + this.tags.BLOCK_END))) { + this.in_code = false; + if (this.trimBlocks) { + cur = this.current(); + if (cur === "\n") { + this.forward(); + } else if (cur === "\r") { + this.forward(); + cur = this.current(); + if (cur === "\n") { + this.forward(); + } else { + this.back(); + } + } + } + return token(TOKEN_BLOCK_END, tok, lineno, colno); + } else if ((tok = this._extractString(this.tags.VARIABLE_END)) || (tok = this._extractString("-" + this.tags.VARIABLE_END))) { + this.in_code = false; + return token(TOKEN_VARIABLE_END, tok, lineno, colno); + } else if (cur === "r" && this.str.charAt(this.index + 1) === "/") { + this.forwardN(2); + var regexBody = ""; + while (!this.isFinished()) { + if (this.current() === "/" && this.previous() !== "\\") { + this.forward(); + break; + } else { + regexBody += this.current(); + this.forward(); + } + } + var POSSIBLE_FLAGS = ["g", "i", "m", "y"]; + var regexFlags = ""; + while (!this.isFinished()) { + var isCurrentAFlag = POSSIBLE_FLAGS.indexOf(this.current()) !== -1; + if (isCurrentAFlag) { + regexFlags += this.current(); + this.forward(); + } else { + break; + } + } + return token(TOKEN_REGEX, { + body: regexBody, + flags: regexFlags + }, lineno, colno); + } else if (delimChars.indexOf(cur) !== -1) { + this.forward(); + var complexOps = ["==", "===", "!=", "!==", "<=", ">=", "//", "**"]; + var curComplex = cur + this.current(); + var type; + if (lib2.indexOf(complexOps, curComplex) !== -1) { + this.forward(); + cur = curComplex; + if (lib2.indexOf(complexOps, curComplex + this.current()) !== -1) { + cur = curComplex + this.current(); + this.forward(); + } + } + switch (cur) { + case "(": + type = TOKEN_LEFT_PAREN; + break; + case ")": + type = TOKEN_RIGHT_PAREN; + break; + case "[": + type = TOKEN_LEFT_BRACKET; + break; + case "]": + type = TOKEN_RIGHT_BRACKET; + break; + case "{": + type = TOKEN_LEFT_CURLY; + break; + case "}": + type = TOKEN_RIGHT_CURLY; + break; + case ",": + type = TOKEN_COMMA; + break; + case ":": + type = TOKEN_COLON; + break; + case "~": + type = TOKEN_TILDE; + break; + case "|": + type = TOKEN_PIPE; + break; + default: + type = TOKEN_OPERATOR; + } + return token(type, cur, lineno, colno); + } else { + tok = this._extractUntil(whitespaceChars + delimChars); + if (tok.match(/^[-+]?[0-9]+$/)) { + if (this.current() === ".") { + this.forward(); + var dec = this._extract(intChars); + return token(TOKEN_FLOAT, tok + "." + dec, lineno, colno); + } else { + return token(TOKEN_INT, tok, lineno, colno); + } + } else if (tok.match(/^(true|false)$/)) { + return token(TOKEN_BOOLEAN, tok, lineno, colno); + } else if (tok === "none") { + return token(TOKEN_NONE, tok, lineno, colno); + } else if (tok === "null") { + return token(TOKEN_NONE, tok, lineno, colno); + } else if (tok) { + return token(TOKEN_SYMBOL, tok, lineno, colno); + } else { + throw new Error("Unexpected value while parsing: " + tok); + } + } + } else { + var beginChars = this.tags.BLOCK_START.charAt(0) + this.tags.VARIABLE_START.charAt(0) + this.tags.COMMENT_START.charAt(0) + this.tags.COMMENT_END.charAt(0); + if (this.isFinished()) { + return null; + } else if ((tok = this._extractString(this.tags.BLOCK_START + "-")) || (tok = this._extractString(this.tags.BLOCK_START))) { + this.in_code = true; + return token(TOKEN_BLOCK_START, tok, lineno, colno); + } else if ((tok = this._extractString(this.tags.VARIABLE_START + "-")) || (tok = this._extractString(this.tags.VARIABLE_START))) { + this.in_code = true; + return token(TOKEN_VARIABLE_START, tok, lineno, colno); + } else { + tok = ""; + var data; + var inComment = false; + if (this._matches(this.tags.COMMENT_START)) { + inComment = true; + tok = this._extractString(this.tags.COMMENT_START); + } + while ((data = this._extractUntil(beginChars)) !== null) { + tok += data; + if ((this._matches(this.tags.BLOCK_START) || this._matches(this.tags.VARIABLE_START) || this._matches(this.tags.COMMENT_START)) && !inComment) { + if (this.lstripBlocks && this._matches(this.tags.BLOCK_START) && this.colno > 0 && this.colno <= tok.length) { + var lastLine = tok.slice(-this.colno); + if (/^\s+$/.test(lastLine)) { + tok = tok.slice(0, -this.colno); + if (!tok.length) { + return this.nextToken(); + } + } + } + break; + } else if (this._matches(this.tags.COMMENT_END)) { + if (!inComment) { + throw new Error("unexpected end of comment"); + } + tok += this._extractString(this.tags.COMMENT_END); + break; + } else { + tok += this.current(); + this.forward(); + } + } + if (data === null && inComment) { + throw new Error("expected end of comment, got end of file"); + } + return token(inComment ? TOKEN_COMMENT : TOKEN_DATA, tok, lineno, colno); + } + } + }; + _proto._parseString = function _parseString(delimiter) { + this.forward(); + var str = ""; + while (!this.isFinished() && this.current() !== delimiter) { + var cur = this.current(); + if (cur === "\\") { + this.forward(); + switch (this.current()) { + case "n": + str += "\n"; + break; + case "t": + str += " "; + break; + case "r": + str += "\r"; + break; + default: + str += this.current(); + } + this.forward(); + } else { + str += cur; + this.forward(); + } + } + this.forward(); + return str; + }; + _proto._matches = function _matches(str) { + if (this.index + str.length > this.len) { + return null; + } + var m = this.str.slice(this.index, this.index + str.length); + return m === str; + }; + _proto._extractString = function _extractString(str) { + if (this._matches(str)) { + this.forwardN(str.length); + return str; + } + return null; + }; + _proto._extractUntil = function _extractUntil(charString) { + return this._extractMatching(true, charString || ""); + }; + _proto._extract = function _extract(charString) { + return this._extractMatching(false, charString); + }; + _proto._extractMatching = function _extractMatching(breakOnMatch, charString) { + if (this.isFinished()) { + return null; + } + var first = charString.indexOf(this.current()); + if (breakOnMatch && first === -1 || !breakOnMatch && first !== -1) { + var t = this.current(); + this.forward(); + var idx = charString.indexOf(this.current()); + while ((breakOnMatch && idx === -1 || !breakOnMatch && idx !== -1) && !this.isFinished()) { + t += this.current(); + this.forward(); + idx = charString.indexOf(this.current()); + } + return t; + } + return ""; + }; + _proto._extractRegex = function _extractRegex(regex) { + var matches = this.currentStr().match(regex); + if (!matches) { + return null; + } + this.forwardN(matches[0].length); + return matches; + }; + _proto.isFinished = function isFinished() { + return this.index >= this.len; + }; + _proto.forwardN = function forwardN(n) { + for (var i = 0; i < n; i++) { + this.forward(); + } + }; + _proto.forward = function forward() { + this.index++; + if (this.previous() === "\n") { + this.lineno++; + this.colno = 0; + } else { + this.colno++; + } + }; + _proto.backN = function backN(n) { + for (var i = 0; i < n; i++) { + this.back(); + } + }; + _proto.back = function back() { + this.index--; + if (this.current() === "\n") { + this.lineno--; + var idx = this.src.lastIndexOf("\n", this.index - 1); + if (idx === -1) { + this.colno = this.index; + } else { + this.colno = this.index - idx; + } + } else { + this.colno--; + } + }; + _proto.current = function current() { + if (!this.isFinished()) { + return this.str.charAt(this.index); + } + return ""; + }; + _proto.currentStr = function currentStr() { + if (!this.isFinished()) { + return this.str.substr(this.index); + } + return ""; + }; + _proto.previous = function previous() { + return this.str.charAt(this.index - 1); + }; + return Tokenizer2; + }(); + module2.exports = { + lex: function lex(src, opts) { + return new Tokenizer(src, opts); + }, + TOKEN_STRING, + TOKEN_WHITESPACE, + TOKEN_DATA, + TOKEN_BLOCK_START, + TOKEN_BLOCK_END, + TOKEN_VARIABLE_START, + TOKEN_VARIABLE_END, + TOKEN_COMMENT, + TOKEN_LEFT_PAREN, + TOKEN_RIGHT_PAREN, + TOKEN_LEFT_BRACKET, + TOKEN_RIGHT_BRACKET, + TOKEN_LEFT_CURLY, + TOKEN_RIGHT_CURLY, + TOKEN_OPERATOR, + TOKEN_COMMA, + TOKEN_COLON, + TOKEN_TILDE, + TOKEN_PIPE, + TOKEN_INT, + TOKEN_FLOAT, + TOKEN_BOOLEAN, + TOKEN_NONE, + TOKEN_SYMBOL, + TOKEN_SPECIAL, + TOKEN_REGEX + }; +}); + +// ../../node_modules/nunjucks/src/object.js +var require_object = __commonJS((exports2, module2) => { + "use strict"; + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) + descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) + _defineProperties(Constructor.prototype, protoProps); + if (staticProps) + _defineProperties(Constructor, staticProps); + return Constructor; + } + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var EventEmitter = require("events"); + var lib2 = require_lib(); + function parentWrap(parent, prop) { + if (typeof parent !== "function" || typeof prop !== "function") { + return prop; + } + return function wrap() { + var tmp = this.parent; + this.parent = parent; + var res = prop.apply(this, arguments); + this.parent = tmp; + return res; + }; + } + function extendClass(cls, name, props) { + props = props || {}; + lib2.keys(props).forEach(function(k) { + props[k] = parentWrap(cls.prototype[k], props[k]); + }); + var subclass = /* @__PURE__ */ function(_cls) { + _inheritsLoose(subclass2, _cls); + function subclass2() { + return _cls.apply(this, arguments) || this; + } + _createClass(subclass2, [{ + key: "typename", + get: function get() { + return name; + } + }]); + return subclass2; + }(cls); + lib2._assign(subclass.prototype, props); + return subclass; + } + var Obj = /* @__PURE__ */ function() { + function Obj2() { + this.init.apply(this, arguments); + } + var _proto = Obj2.prototype; + _proto.init = function init() { + }; + Obj2.extend = function extend(name, props) { + if (typeof name === "object") { + props = name; + name = "anonymous"; + } + return extendClass(this, name, props); + }; + _createClass(Obj2, [{ + key: "typename", + get: function get() { + return this.constructor.name; + } + }]); + return Obj2; + }(); + var EmitterObj = /* @__PURE__ */ function(_EventEmitter) { + _inheritsLoose(EmitterObj2, _EventEmitter); + function EmitterObj2() { + var _this2; + var _this; + _this = _EventEmitter.call(this) || this; + (_this2 = _this).init.apply(_this2, arguments); + return _this; + } + var _proto2 = EmitterObj2.prototype; + _proto2.init = function init() { + }; + EmitterObj2.extend = function extend(name, props) { + if (typeof name === "object") { + props = name; + name = "anonymous"; + } + return extendClass(this, name, props); + }; + _createClass(EmitterObj2, [{ + key: "typename", + get: function get() { + return this.constructor.name; + } + }]); + return EmitterObj2; + }(EventEmitter); + module2.exports = { + Obj, + EmitterObj + }; +}); + +// ../../node_modules/nunjucks/src/nodes.js +var require_nodes = __commonJS((exports2, module2) => { + "use strict"; + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) + descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) + _defineProperties(Constructor.prototype, protoProps); + if (staticProps) + _defineProperties(Constructor, staticProps); + return Constructor; + } + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var _require2 = require_object(); + var Obj = _require2.Obj; + function traverseAndCheck(obj, type, results) { + if (obj instanceof type) { + results.push(obj); + } + if (obj instanceof Node) { + obj.findAll(type, results); + } + } + var Node = /* @__PURE__ */ function(_Obj) { + _inheritsLoose(Node2, _Obj); + function Node2() { + return _Obj.apply(this, arguments) || this; + } + var _proto = Node2.prototype; + _proto.init = function init(lineno, colno) { + var _arguments = arguments, _this = this; + for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + args[_key - 2] = arguments[_key]; + } + this.lineno = lineno; + this.colno = colno; + this.fields.forEach(function(field, i) { + var val = _arguments[i + 2]; + if (val === void 0) { + val = null; + } + _this[field] = val; + }); + }; + _proto.findAll = function findAll(type, results) { + var _this2 = this; + results = results || []; + if (this instanceof NodeList) { + this.children.forEach(function(child) { + return traverseAndCheck(child, type, results); + }); + } else { + this.fields.forEach(function(field) { + return traverseAndCheck(_this2[field], type, results); + }); + } + return results; + }; + _proto.iterFields = function iterFields(func) { + var _this3 = this; + this.fields.forEach(function(field) { + func(_this3[field], field); + }); + }; + return Node2; + }(Obj); + var Value = /* @__PURE__ */ function(_Node) { + _inheritsLoose(Value2, _Node); + function Value2() { + return _Node.apply(this, arguments) || this; + } + _createClass(Value2, [{ + key: "typename", + get: function get() { + return "Value"; + } + }, { + key: "fields", + get: function get() { + return ["value"]; + } + }]); + return Value2; + }(Node); + var NodeList = /* @__PURE__ */ function(_Node2) { + _inheritsLoose(NodeList2, _Node2); + function NodeList2() { + return _Node2.apply(this, arguments) || this; + } + var _proto2 = NodeList2.prototype; + _proto2.init = function init(lineno, colno, nodes2) { + _Node2.prototype.init.call(this, lineno, colno, nodes2 || []); + }; + _proto2.addChild = function addChild(node) { + this.children.push(node); + }; + _createClass(NodeList2, [{ + key: "typename", + get: function get() { + return "NodeList"; + } + }, { + key: "fields", + get: function get() { + return ["children"]; + } + }]); + return NodeList2; + }(Node); + var Root = NodeList.extend("Root"); + var Literal = Value.extend("Literal"); + var Symbol2 = Value.extend("Symbol"); + var Group = NodeList.extend("Group"); + var ArrayNode = NodeList.extend("Array"); + var Pair = Node.extend("Pair", { + fields: ["key", "value"] + }); + var Dict = NodeList.extend("Dict"); + var LookupVal = Node.extend("LookupVal", { + fields: ["target", "val"] + }); + var If = Node.extend("If", { + fields: ["cond", "body", "else_"] + }); + var IfAsync = If.extend("IfAsync"); + var InlineIf = Node.extend("InlineIf", { + fields: ["cond", "body", "else_"] + }); + var For = Node.extend("For", { + fields: ["arr", "name", "body", "else_"] + }); + var AsyncEach = For.extend("AsyncEach"); + var AsyncAll = For.extend("AsyncAll"); + var Macro = Node.extend("Macro", { + fields: ["name", "args", "body"] + }); + var Caller = Macro.extend("Caller"); + var Import = Node.extend("Import", { + fields: ["template", "target", "withContext"] + }); + var FromImport = /* @__PURE__ */ function(_Node3) { + _inheritsLoose(FromImport2, _Node3); + function FromImport2() { + return _Node3.apply(this, arguments) || this; + } + var _proto3 = FromImport2.prototype; + _proto3.init = function init(lineno, colno, template, names, withContext) { + _Node3.prototype.init.call(this, lineno, colno, template, names || new NodeList(), withContext); + }; + _createClass(FromImport2, [{ + key: "typename", + get: function get() { + return "FromImport"; + } + }, { + key: "fields", + get: function get() { + return ["template", "names", "withContext"]; + } + }]); + return FromImport2; + }(Node); + var FunCall = Node.extend("FunCall", { + fields: ["name", "args"] + }); + var Filter = FunCall.extend("Filter"); + var FilterAsync = Filter.extend("FilterAsync", { + fields: ["name", "args", "symbol"] + }); + var KeywordArgs = Dict.extend("KeywordArgs"); + var Block = Node.extend("Block", { + fields: ["name", "body"] + }); + var Super = Node.extend("Super", { + fields: ["blockName", "symbol"] + }); + var TemplateRef = Node.extend("TemplateRef", { + fields: ["template"] + }); + var Extends = TemplateRef.extend("Extends"); + var Include = Node.extend("Include", { + fields: ["template", "ignoreMissing"] + }); + var Set2 = Node.extend("Set", { + fields: ["targets", "value"] + }); + var Switch = Node.extend("Switch", { + fields: ["expr", "cases", "default"] + }); + var Case = Node.extend("Case", { + fields: ["cond", "body"] + }); + var Output = NodeList.extend("Output"); + var Capture = Node.extend("Capture", { + fields: ["body"] + }); + var TemplateData = Literal.extend("TemplateData"); + var UnaryOp = Node.extend("UnaryOp", { + fields: ["target"] + }); + var BinOp = Node.extend("BinOp", { + fields: ["left", "right"] + }); + var In = BinOp.extend("In"); + var Is = BinOp.extend("Is"); + var Or = BinOp.extend("Or"); + var And = BinOp.extend("And"); + var Not = UnaryOp.extend("Not"); + var Add = BinOp.extend("Add"); + var Concat = BinOp.extend("Concat"); + var Sub = BinOp.extend("Sub"); + var Mul = BinOp.extend("Mul"); + var Div = BinOp.extend("Div"); + var FloorDiv = BinOp.extend("FloorDiv"); + var Mod = BinOp.extend("Mod"); + var Pow = BinOp.extend("Pow"); + var Neg = UnaryOp.extend("Neg"); + var Pos = UnaryOp.extend("Pos"); + var Compare = Node.extend("Compare", { + fields: ["expr", "ops"] + }); + var CompareOperand = Node.extend("CompareOperand", { + fields: ["expr", "type"] + }); + var CallExtension = Node.extend("CallExtension", { + init: function init(ext, prop, args, contentArgs) { + this.parent(); + this.extName = ext.__name || ext; + this.prop = prop; + this.args = args || new NodeList(); + this.contentArgs = contentArgs || []; + this.autoescape = ext.autoescape; + }, + fields: ["extName", "prop", "args", "contentArgs"] + }); + var CallExtensionAsync = CallExtension.extend("CallExtensionAsync"); + function print(str, indent, inline) { + var lines = str.split("\n"); + lines.forEach(function(line, i) { + if (line && (inline && i > 0 || !inline)) { + process.stdout.write(" ".repeat(indent)); + } + var nl = i === lines.length - 1 ? "" : "\n"; + process.stdout.write("" + line + nl); + }); + } + function printNodes(node, indent) { + indent = indent || 0; + print(node.typename + ": ", indent); + if (node instanceof NodeList) { + print("\n"); + node.children.forEach(function(n) { + printNodes(n, indent + 2); + }); + } else if (node instanceof CallExtension) { + print(node.extName + "." + node.prop + "\n"); + if (node.args) { + printNodes(node.args, indent + 2); + } + if (node.contentArgs) { + node.contentArgs.forEach(function(n) { + printNodes(n, indent + 2); + }); + } + } else { + var nodes2 = []; + var props = null; + node.iterFields(function(val, fieldName) { + if (val instanceof Node) { + nodes2.push([fieldName, val]); + } else { + props = props || {}; + props[fieldName] = val; + } + }); + if (props) { + print(JSON.stringify(props, null, 2) + "\n", null, true); + } else { + print("\n"); + } + nodes2.forEach(function(_ref) { + var fieldName = _ref[0], n = _ref[1]; + print("[" + fieldName + "] =>", indent + 2); + printNodes(n, indent + 4); + }); + } + } + module2.exports = { + Node, + Root, + NodeList, + Value, + Literal, + Symbol: Symbol2, + Group, + Array: ArrayNode, + Pair, + Dict, + Output, + Capture, + TemplateData, + If, + IfAsync, + InlineIf, + For, + AsyncEach, + AsyncAll, + Macro, + Caller, + Import, + FromImport, + FunCall, + Filter, + FilterAsync, + KeywordArgs, + Block, + Super, + Extends, + Include, + Set: Set2, + Switch, + Case, + LookupVal, + BinOp, + In, + Is, + Or, + And, + Not, + Add, + Concat, + Sub, + Mul, + Div, + FloorDiv, + Mod, + Pow, + Neg, + Pos, + Compare, + CompareOperand, + CallExtension, + CallExtensionAsync, + printNodes + }; +}); + +// ../../node_modules/nunjucks/src/parser.js +var require_parser = __commonJS((exports2, module2) => { + "use strict"; + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var lexer2 = require_lexer(); + var nodes2 = require_nodes(); + var Obj = require_object().Obj; + var lib2 = require_lib(); + var Parser = /* @__PURE__ */ function(_Obj) { + _inheritsLoose(Parser2, _Obj); + function Parser2() { + return _Obj.apply(this, arguments) || this; + } + var _proto = Parser2.prototype; + _proto.init = function init(tokens) { + this.tokens = tokens; + this.peeked = null; + this.breakOnBlocks = null; + this.dropLeadingWhitespace = false; + this.extensions = []; + }; + _proto.nextToken = function nextToken(withWhitespace) { + var tok; + if (this.peeked) { + if (!withWhitespace && this.peeked.type === lexer2.TOKEN_WHITESPACE) { + this.peeked = null; + } else { + tok = this.peeked; + this.peeked = null; + return tok; + } + } + tok = this.tokens.nextToken(); + if (!withWhitespace) { + while (tok && tok.type === lexer2.TOKEN_WHITESPACE) { + tok = this.tokens.nextToken(); + } + } + return tok; + }; + _proto.peekToken = function peekToken() { + this.peeked = this.peeked || this.nextToken(); + return this.peeked; + }; + _proto.pushToken = function pushToken(tok) { + if (this.peeked) { + throw new Error("pushToken: can only push one token on between reads"); + } + this.peeked = tok; + }; + _proto.error = function error(msg, lineno, colno) { + if (lineno === void 0 || colno === void 0) { + var tok = this.peekToken() || {}; + lineno = tok.lineno; + colno = tok.colno; + } + if (lineno !== void 0) { + lineno += 1; + } + if (colno !== void 0) { + colno += 1; + } + return new lib2.TemplateError(msg, lineno, colno); + }; + _proto.fail = function fail(msg, lineno, colno) { + throw this.error(msg, lineno, colno); + }; + _proto.skip = function skip(type) { + var tok = this.nextToken(); + if (!tok || tok.type !== type) { + this.pushToken(tok); + return false; + } + return true; + }; + _proto.expect = function expect(type) { + var tok = this.nextToken(); + if (tok.type !== type) { + this.fail("expected " + type + ", got " + tok.type, tok.lineno, tok.colno); + } + return tok; + }; + _proto.skipValue = function skipValue(type, val) { + var tok = this.nextToken(); + if (!tok || tok.type !== type || tok.value !== val) { + this.pushToken(tok); + return false; + } + return true; + }; + _proto.skipSymbol = function skipSymbol(val) { + return this.skipValue(lexer2.TOKEN_SYMBOL, val); + }; + _proto.advanceAfterBlockEnd = function advanceAfterBlockEnd(name) { + var tok; + if (!name) { + tok = this.peekToken(); + if (!tok) { + this.fail("unexpected end of file"); + } + if (tok.type !== lexer2.TOKEN_SYMBOL) { + this.fail("advanceAfterBlockEnd: expected symbol token or explicit name to be passed"); + } + name = this.nextToken().value; + } + tok = this.nextToken(); + if (tok && tok.type === lexer2.TOKEN_BLOCK_END) { + if (tok.value.charAt(0) === "-") { + this.dropLeadingWhitespace = true; + } + } else { + this.fail("expected block end in " + name + " statement"); + } + return tok; + }; + _proto.advanceAfterVariableEnd = function advanceAfterVariableEnd() { + var tok = this.nextToken(); + if (tok && tok.type === lexer2.TOKEN_VARIABLE_END) { + this.dropLeadingWhitespace = tok.value.charAt(tok.value.length - this.tokens.tags.VARIABLE_END.length - 1) === "-"; + } else { + this.pushToken(tok); + this.fail("expected variable end"); + } + }; + _proto.parseFor = function parseFor() { + var forTok = this.peekToken(); + var node; + var endBlock; + if (this.skipSymbol("for")) { + node = new nodes2.For(forTok.lineno, forTok.colno); + endBlock = "endfor"; + } else if (this.skipSymbol("asyncEach")) { + node = new nodes2.AsyncEach(forTok.lineno, forTok.colno); + endBlock = "endeach"; + } else if (this.skipSymbol("asyncAll")) { + node = new nodes2.AsyncAll(forTok.lineno, forTok.colno); + endBlock = "endall"; + } else { + this.fail("parseFor: expected for{Async}", forTok.lineno, forTok.colno); + } + node.name = this.parsePrimary(); + if (!(node.name instanceof nodes2.Symbol)) { + this.fail("parseFor: variable name expected for loop"); + } + var type = this.peekToken().type; + if (type === lexer2.TOKEN_COMMA) { + var key = node.name; + node.name = new nodes2.Array(key.lineno, key.colno); + node.name.addChild(key); + while (this.skip(lexer2.TOKEN_COMMA)) { + var prim = this.parsePrimary(); + node.name.addChild(prim); + } + } + if (!this.skipSymbol("in")) { + this.fail('parseFor: expected "in" keyword for loop', forTok.lineno, forTok.colno); + } + node.arr = this.parseExpression(); + this.advanceAfterBlockEnd(forTok.value); + node.body = this.parseUntilBlocks(endBlock, "else"); + if (this.skipSymbol("else")) { + this.advanceAfterBlockEnd("else"); + node.else_ = this.parseUntilBlocks(endBlock); + } + this.advanceAfterBlockEnd(); + return node; + }; + _proto.parseMacro = function parseMacro() { + var macroTok = this.peekToken(); + if (!this.skipSymbol("macro")) { + this.fail("expected macro"); + } + var name = this.parsePrimary(true); + var args = this.parseSignature(); + var node = new nodes2.Macro(macroTok.lineno, macroTok.colno, name, args); + this.advanceAfterBlockEnd(macroTok.value); + node.body = this.parseUntilBlocks("endmacro"); + this.advanceAfterBlockEnd(); + return node; + }; + _proto.parseCall = function parseCall() { + var callTok = this.peekToken(); + if (!this.skipSymbol("call")) { + this.fail("expected call"); + } + var callerArgs = this.parseSignature(true) || new nodes2.NodeList(); + var macroCall = this.parsePrimary(); + this.advanceAfterBlockEnd(callTok.value); + var body = this.parseUntilBlocks("endcall"); + this.advanceAfterBlockEnd(); + var callerName = new nodes2.Symbol(callTok.lineno, callTok.colno, "caller"); + var callerNode = new nodes2.Caller(callTok.lineno, callTok.colno, callerName, callerArgs, body); + var args = macroCall.args.children; + if (!(args[args.length - 1] instanceof nodes2.KeywordArgs)) { + args.push(new nodes2.KeywordArgs()); + } + var kwargs = args[args.length - 1]; + kwargs.addChild(new nodes2.Pair(callTok.lineno, callTok.colno, callerName, callerNode)); + return new nodes2.Output(callTok.lineno, callTok.colno, [macroCall]); + }; + _proto.parseWithContext = function parseWithContext() { + var tok = this.peekToken(); + var withContext = null; + if (this.skipSymbol("with")) { + withContext = true; + } else if (this.skipSymbol("without")) { + withContext = false; + } + if (withContext !== null) { + if (!this.skipSymbol("context")) { + this.fail("parseFrom: expected context after with/without", tok.lineno, tok.colno); + } + } + return withContext; + }; + _proto.parseImport = function parseImport() { + var importTok = this.peekToken(); + if (!this.skipSymbol("import")) { + this.fail("parseImport: expected import", importTok.lineno, importTok.colno); + } + var template = this.parseExpression(); + if (!this.skipSymbol("as")) { + this.fail('parseImport: expected "as" keyword', importTok.lineno, importTok.colno); + } + var target = this.parseExpression(); + var withContext = this.parseWithContext(); + var node = new nodes2.Import(importTok.lineno, importTok.colno, template, target, withContext); + this.advanceAfterBlockEnd(importTok.value); + return node; + }; + _proto.parseFrom = function parseFrom() { + var fromTok = this.peekToken(); + if (!this.skipSymbol("from")) { + this.fail("parseFrom: expected from"); + } + var template = this.parseExpression(); + if (!this.skipSymbol("import")) { + this.fail("parseFrom: expected import", fromTok.lineno, fromTok.colno); + } + var names = new nodes2.NodeList(); + var withContext; + while (1) { + var nextTok = this.peekToken(); + if (nextTok.type === lexer2.TOKEN_BLOCK_END) { + if (!names.children.length) { + this.fail("parseFrom: Expected at least one import name", fromTok.lineno, fromTok.colno); + } + if (nextTok.value.charAt(0) === "-") { + this.dropLeadingWhitespace = true; + } + this.nextToken(); + break; + } + if (names.children.length > 0 && !this.skip(lexer2.TOKEN_COMMA)) { + this.fail("parseFrom: expected comma", fromTok.lineno, fromTok.colno); + } + var name = this.parsePrimary(); + if (name.value.charAt(0) === "_") { + this.fail("parseFrom: names starting with an underscore cannot be imported", name.lineno, name.colno); + } + if (this.skipSymbol("as")) { + var alias = this.parsePrimary(); + names.addChild(new nodes2.Pair(name.lineno, name.colno, name, alias)); + } else { + names.addChild(name); + } + withContext = this.parseWithContext(); + } + return new nodes2.FromImport(fromTok.lineno, fromTok.colno, template, names, withContext); + }; + _proto.parseBlock = function parseBlock() { + var tag = this.peekToken(); + if (!this.skipSymbol("block")) { + this.fail("parseBlock: expected block", tag.lineno, tag.colno); + } + var node = new nodes2.Block(tag.lineno, tag.colno); + node.name = this.parsePrimary(); + if (!(node.name instanceof nodes2.Symbol)) { + this.fail("parseBlock: variable name expected", tag.lineno, tag.colno); + } + this.advanceAfterBlockEnd(tag.value); + node.body = this.parseUntilBlocks("endblock"); + this.skipSymbol("endblock"); + this.skipSymbol(node.name.value); + var tok = this.peekToken(); + if (!tok) { + this.fail("parseBlock: expected endblock, got end of file"); + } + this.advanceAfterBlockEnd(tok.value); + return node; + }; + _proto.parseExtends = function parseExtends() { + var tagName = "extends"; + var tag = this.peekToken(); + if (!this.skipSymbol(tagName)) { + this.fail("parseTemplateRef: expected " + tagName); + } + var node = new nodes2.Extends(tag.lineno, tag.colno); + node.template = this.parseExpression(); + this.advanceAfterBlockEnd(tag.value); + return node; + }; + _proto.parseInclude = function parseInclude() { + var tagName = "include"; + var tag = this.peekToken(); + if (!this.skipSymbol(tagName)) { + this.fail("parseInclude: expected " + tagName); + } + var node = new nodes2.Include(tag.lineno, tag.colno); + node.template = this.parseExpression(); + if (this.skipSymbol("ignore") && this.skipSymbol("missing")) { + node.ignoreMissing = true; + } + this.advanceAfterBlockEnd(tag.value); + return node; + }; + _proto.parseIf = function parseIf() { + var tag = this.peekToken(); + var node; + if (this.skipSymbol("if") || this.skipSymbol("elif") || this.skipSymbol("elseif")) { + node = new nodes2.If(tag.lineno, tag.colno); + } else if (this.skipSymbol("ifAsync")) { + node = new nodes2.IfAsync(tag.lineno, tag.colno); + } else { + this.fail("parseIf: expected if, elif, or elseif", tag.lineno, tag.colno); + } + node.cond = this.parseExpression(); + this.advanceAfterBlockEnd(tag.value); + node.body = this.parseUntilBlocks("elif", "elseif", "else", "endif"); + var tok = this.peekToken(); + switch (tok && tok.value) { + case "elseif": + case "elif": + node.else_ = this.parseIf(); + break; + case "else": + this.advanceAfterBlockEnd(); + node.else_ = this.parseUntilBlocks("endif"); + this.advanceAfterBlockEnd(); + break; + case "endif": + node.else_ = null; + this.advanceAfterBlockEnd(); + break; + default: + this.fail("parseIf: expected elif, else, or endif, got end of file"); + } + return node; + }; + _proto.parseSet = function parseSet() { + var tag = this.peekToken(); + if (!this.skipSymbol("set")) { + this.fail("parseSet: expected set", tag.lineno, tag.colno); + } + var node = new nodes2.Set(tag.lineno, tag.colno, []); + var target; + while (target = this.parsePrimary()) { + node.targets.push(target); + if (!this.skip(lexer2.TOKEN_COMMA)) { + break; + } + } + if (!this.skipValue(lexer2.TOKEN_OPERATOR, "=")) { + if (!this.skip(lexer2.TOKEN_BLOCK_END)) { + this.fail("parseSet: expected = or block end in set tag", tag.lineno, tag.colno); + } else { + node.body = new nodes2.Capture(tag.lineno, tag.colno, this.parseUntilBlocks("endset")); + node.value = null; + this.advanceAfterBlockEnd(); + } + } else { + node.value = this.parseExpression(); + this.advanceAfterBlockEnd(tag.value); + } + return node; + }; + _proto.parseSwitch = function parseSwitch() { + var switchStart = "switch"; + var switchEnd = "endswitch"; + var caseStart = "case"; + var caseDefault = "default"; + var tag = this.peekToken(); + if (!this.skipSymbol(switchStart) && !this.skipSymbol(caseStart) && !this.skipSymbol(caseDefault)) { + this.fail('parseSwitch: expected "switch," "case" or "default"', tag.lineno, tag.colno); + } + var expr = this.parseExpression(); + this.advanceAfterBlockEnd(switchStart); + this.parseUntilBlocks(caseStart, caseDefault, switchEnd); + var tok = this.peekToken(); + var cases = []; + var defaultCase; + do { + this.skipSymbol(caseStart); + var cond = this.parseExpression(); + this.advanceAfterBlockEnd(switchStart); + var body = this.parseUntilBlocks(caseStart, caseDefault, switchEnd); + cases.push(new nodes2.Case(tok.line, tok.col, cond, body)); + tok = this.peekToken(); + } while (tok && tok.value === caseStart); + switch (tok.value) { + case caseDefault: + this.advanceAfterBlockEnd(); + defaultCase = this.parseUntilBlocks(switchEnd); + this.advanceAfterBlockEnd(); + break; + case switchEnd: + this.advanceAfterBlockEnd(); + break; + default: + this.fail('parseSwitch: expected "case," "default" or "endswitch," got EOF.'); + } + return new nodes2.Switch(tag.lineno, tag.colno, expr, cases, defaultCase); + }; + _proto.parseStatement = function parseStatement() { + var tok = this.peekToken(); + var node; + if (tok.type !== lexer2.TOKEN_SYMBOL) { + this.fail("tag name expected", tok.lineno, tok.colno); + } + if (this.breakOnBlocks && lib2.indexOf(this.breakOnBlocks, tok.value) !== -1) { + return null; + } + switch (tok.value) { + case "raw": + return this.parseRaw(); + case "verbatim": + return this.parseRaw("verbatim"); + case "if": + case "ifAsync": + return this.parseIf(); + case "for": + case "asyncEach": + case "asyncAll": + return this.parseFor(); + case "block": + return this.parseBlock(); + case "extends": + return this.parseExtends(); + case "include": + return this.parseInclude(); + case "set": + return this.parseSet(); + case "macro": + return this.parseMacro(); + case "call": + return this.parseCall(); + case "import": + return this.parseImport(); + case "from": + return this.parseFrom(); + case "filter": + return this.parseFilterStatement(); + case "switch": + return this.parseSwitch(); + default: + if (this.extensions.length) { + for (var i = 0; i < this.extensions.length; i++) { + var ext = this.extensions[i]; + if (lib2.indexOf(ext.tags || [], tok.value) !== -1) { + return ext.parse(this, nodes2, lexer2); + } + } + } + this.fail("unknown block tag: " + tok.value, tok.lineno, tok.colno); + } + return node; + }; + _proto.parseRaw = function parseRaw(tagName) { + tagName = tagName || "raw"; + var endTagName = "end" + tagName; + var rawBlockRegex = new RegExp("([\\s\\S]*?){%\\s*(" + tagName + "|" + endTagName + ")\\s*(?=%})%}"); + var rawLevel = 1; + var str = ""; + var matches = null; + var begun = this.advanceAfterBlockEnd(); + while ((matches = this.tokens._extractRegex(rawBlockRegex)) && rawLevel > 0) { + var all = matches[0]; + var pre = matches[1]; + var blockName = matches[2]; + if (blockName === tagName) { + rawLevel += 1; + } else if (blockName === endTagName) { + rawLevel -= 1; + } + if (rawLevel === 0) { + str += pre; + this.tokens.backN(all.length - pre.length); + } else { + str += all; + } + } + return new nodes2.Output(begun.lineno, begun.colno, [new nodes2.TemplateData(begun.lineno, begun.colno, str)]); + }; + _proto.parsePostfix = function parsePostfix(node) { + var lookup; + var tok = this.peekToken(); + while (tok) { + if (tok.type === lexer2.TOKEN_LEFT_PAREN) { + node = new nodes2.FunCall(tok.lineno, tok.colno, node, this.parseSignature()); + } else if (tok.type === lexer2.TOKEN_LEFT_BRACKET) { + lookup = this.parseAggregate(); + if (lookup.children.length > 1) { + this.fail("invalid index"); + } + node = new nodes2.LookupVal(tok.lineno, tok.colno, node, lookup.children[0]); + } else if (tok.type === lexer2.TOKEN_OPERATOR && tok.value === ".") { + this.nextToken(); + var val = this.nextToken(); + if (val.type !== lexer2.TOKEN_SYMBOL) { + this.fail("expected name as lookup value, got " + val.value, val.lineno, val.colno); + } + lookup = new nodes2.Literal(val.lineno, val.colno, val.value); + node = new nodes2.LookupVal(tok.lineno, tok.colno, node, lookup); + } else { + break; + } + tok = this.peekToken(); + } + return node; + }; + _proto.parseExpression = function parseExpression() { + var node = this.parseInlineIf(); + return node; + }; + _proto.parseInlineIf = function parseInlineIf() { + var node = this.parseOr(); + if (this.skipSymbol("if")) { + var condNode = this.parseOr(); + var bodyNode = node; + node = new nodes2.InlineIf(node.lineno, node.colno); + node.body = bodyNode; + node.cond = condNode; + if (this.skipSymbol("else")) { + node.else_ = this.parseOr(); + } else { + node.else_ = null; + } + } + return node; + }; + _proto.parseOr = function parseOr() { + var node = this.parseAnd(); + while (this.skipSymbol("or")) { + var node2 = this.parseAnd(); + node = new nodes2.Or(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseAnd = function parseAnd() { + var node = this.parseNot(); + while (this.skipSymbol("and")) { + var node2 = this.parseNot(); + node = new nodes2.And(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseNot = function parseNot() { + var tok = this.peekToken(); + if (this.skipSymbol("not")) { + return new nodes2.Not(tok.lineno, tok.colno, this.parseNot()); + } + return this.parseIn(); + }; + _proto.parseIn = function parseIn() { + var node = this.parseIs(); + while (1) { + var tok = this.nextToken(); + if (!tok) { + break; + } + var invert = tok.type === lexer2.TOKEN_SYMBOL && tok.value === "not"; + if (!invert) { + this.pushToken(tok); + } + if (this.skipSymbol("in")) { + var node2 = this.parseIs(); + node = new nodes2.In(node.lineno, node.colno, node, node2); + if (invert) { + node = new nodes2.Not(node.lineno, node.colno, node); + } + } else { + if (invert) { + this.pushToken(tok); + } + break; + } + } + return node; + }; + _proto.parseIs = function parseIs() { + var node = this.parseCompare(); + if (this.skipSymbol("is")) { + var not = this.skipSymbol("not"); + var node2 = this.parseCompare(); + node = new nodes2.Is(node.lineno, node.colno, node, node2); + if (not) { + node = new nodes2.Not(node.lineno, node.colno, node); + } + } + return node; + }; + _proto.parseCompare = function parseCompare() { + var compareOps = ["==", "===", "!=", "!==", "<", ">", "<=", ">="]; + var expr = this.parseConcat(); + var ops = []; + while (1) { + var tok = this.nextToken(); + if (!tok) { + break; + } else if (compareOps.indexOf(tok.value) !== -1) { + ops.push(new nodes2.CompareOperand(tok.lineno, tok.colno, this.parseConcat(), tok.value)); + } else { + this.pushToken(tok); + break; + } + } + if (ops.length) { + return new nodes2.Compare(ops[0].lineno, ops[0].colno, expr, ops); + } else { + return expr; + } + }; + _proto.parseConcat = function parseConcat() { + var node = this.parseAdd(); + while (this.skipValue(lexer2.TOKEN_TILDE, "~")) { + var node2 = this.parseAdd(); + node = new nodes2.Concat(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseAdd = function parseAdd() { + var node = this.parseSub(); + while (this.skipValue(lexer2.TOKEN_OPERATOR, "+")) { + var node2 = this.parseSub(); + node = new nodes2.Add(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseSub = function parseSub() { + var node = this.parseMul(); + while (this.skipValue(lexer2.TOKEN_OPERATOR, "-")) { + var node2 = this.parseMul(); + node = new nodes2.Sub(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseMul = function parseMul() { + var node = this.parseDiv(); + while (this.skipValue(lexer2.TOKEN_OPERATOR, "*")) { + var node2 = this.parseDiv(); + node = new nodes2.Mul(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseDiv = function parseDiv() { + var node = this.parseFloorDiv(); + while (this.skipValue(lexer2.TOKEN_OPERATOR, "/")) { + var node2 = this.parseFloorDiv(); + node = new nodes2.Div(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseFloorDiv = function parseFloorDiv() { + var node = this.parseMod(); + while (this.skipValue(lexer2.TOKEN_OPERATOR, "//")) { + var node2 = this.parseMod(); + node = new nodes2.FloorDiv(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseMod = function parseMod() { + var node = this.parsePow(); + while (this.skipValue(lexer2.TOKEN_OPERATOR, "%")) { + var node2 = this.parsePow(); + node = new nodes2.Mod(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parsePow = function parsePow() { + var node = this.parseUnary(); + while (this.skipValue(lexer2.TOKEN_OPERATOR, "**")) { + var node2 = this.parseUnary(); + node = new nodes2.Pow(node.lineno, node.colno, node, node2); + } + return node; + }; + _proto.parseUnary = function parseUnary(noFilters) { + var tok = this.peekToken(); + var node; + if (this.skipValue(lexer2.TOKEN_OPERATOR, "-")) { + node = new nodes2.Neg(tok.lineno, tok.colno, this.parseUnary(true)); + } else if (this.skipValue(lexer2.TOKEN_OPERATOR, "+")) { + node = new nodes2.Pos(tok.lineno, tok.colno, this.parseUnary(true)); + } else { + node = this.parsePrimary(); + } + if (!noFilters) { + node = this.parseFilter(node); + } + return node; + }; + _proto.parsePrimary = function parsePrimary(noPostfix) { + var tok = this.nextToken(); + var val; + var node = null; + if (!tok) { + this.fail("expected expression, got end of file"); + } else if (tok.type === lexer2.TOKEN_STRING) { + val = tok.value; + } else if (tok.type === lexer2.TOKEN_INT) { + val = parseInt(tok.value, 10); + } else if (tok.type === lexer2.TOKEN_FLOAT) { + val = parseFloat(tok.value); + } else if (tok.type === lexer2.TOKEN_BOOLEAN) { + if (tok.value === "true") { + val = true; + } else if (tok.value === "false") { + val = false; + } else { + this.fail("invalid boolean: " + tok.value, tok.lineno, tok.colno); + } + } else if (tok.type === lexer2.TOKEN_NONE) { + val = null; + } else if (tok.type === lexer2.TOKEN_REGEX) { + val = new RegExp(tok.value.body, tok.value.flags); + } + if (val !== void 0) { + node = new nodes2.Literal(tok.lineno, tok.colno, val); + } else if (tok.type === lexer2.TOKEN_SYMBOL) { + node = new nodes2.Symbol(tok.lineno, tok.colno, tok.value); + } else { + this.pushToken(tok); + node = this.parseAggregate(); + } + if (!noPostfix) { + node = this.parsePostfix(node); + } + if (node) { + return node; + } else { + throw this.error("unexpected token: " + tok.value, tok.lineno, tok.colno); + } + }; + _proto.parseFilterName = function parseFilterName() { + var tok = this.expect(lexer2.TOKEN_SYMBOL); + var name = tok.value; + while (this.skipValue(lexer2.TOKEN_OPERATOR, ".")) { + name += "." + this.expect(lexer2.TOKEN_SYMBOL).value; + } + return new nodes2.Symbol(tok.lineno, tok.colno, name); + }; + _proto.parseFilterArgs = function parseFilterArgs(node) { + if (this.peekToken().type === lexer2.TOKEN_LEFT_PAREN) { + var call = this.parsePostfix(node); + return call.args.children; + } + return []; + }; + _proto.parseFilter = function parseFilter(node) { + while (this.skip(lexer2.TOKEN_PIPE)) { + var name = this.parseFilterName(); + node = new nodes2.Filter(name.lineno, name.colno, name, new nodes2.NodeList(name.lineno, name.colno, [node].concat(this.parseFilterArgs(node)))); + } + return node; + }; + _proto.parseFilterStatement = function parseFilterStatement() { + var filterTok = this.peekToken(); + if (!this.skipSymbol("filter")) { + this.fail("parseFilterStatement: expected filter"); + } + var name = this.parseFilterName(); + var args = this.parseFilterArgs(name); + this.advanceAfterBlockEnd(filterTok.value); + var body = new nodes2.Capture(name.lineno, name.colno, this.parseUntilBlocks("endfilter")); + this.advanceAfterBlockEnd(); + var node = new nodes2.Filter(name.lineno, name.colno, name, new nodes2.NodeList(name.lineno, name.colno, [body].concat(args))); + return new nodes2.Output(name.lineno, name.colno, [node]); + }; + _proto.parseAggregate = function parseAggregate() { + var tok = this.nextToken(); + var node; + switch (tok.type) { + case lexer2.TOKEN_LEFT_PAREN: + node = new nodes2.Group(tok.lineno, tok.colno); + break; + case lexer2.TOKEN_LEFT_BRACKET: + node = new nodes2.Array(tok.lineno, tok.colno); + break; + case lexer2.TOKEN_LEFT_CURLY: + node = new nodes2.Dict(tok.lineno, tok.colno); + break; + default: + return null; + } + while (1) { + var type = this.peekToken().type; + if (type === lexer2.TOKEN_RIGHT_PAREN || type === lexer2.TOKEN_RIGHT_BRACKET || type === lexer2.TOKEN_RIGHT_CURLY) { + this.nextToken(); + break; + } + if (node.children.length > 0) { + if (!this.skip(lexer2.TOKEN_COMMA)) { + this.fail("parseAggregate: expected comma after expression", tok.lineno, tok.colno); + } + } + if (node instanceof nodes2.Dict) { + var key = this.parsePrimary(); + if (!this.skip(lexer2.TOKEN_COLON)) { + this.fail("parseAggregate: expected colon after dict key", tok.lineno, tok.colno); + } + var value = this.parseExpression(); + node.addChild(new nodes2.Pair(key.lineno, key.colno, key, value)); + } else { + var expr = this.parseExpression(); + node.addChild(expr); + } + } + return node; + }; + _proto.parseSignature = function parseSignature(tolerant, noParens) { + var tok = this.peekToken(); + if (!noParens && tok.type !== lexer2.TOKEN_LEFT_PAREN) { + if (tolerant) { + return null; + } else { + this.fail("expected arguments", tok.lineno, tok.colno); + } + } + if (tok.type === lexer2.TOKEN_LEFT_PAREN) { + tok = this.nextToken(); + } + var args = new nodes2.NodeList(tok.lineno, tok.colno); + var kwargs = new nodes2.KeywordArgs(tok.lineno, tok.colno); + var checkComma = false; + while (1) { + tok = this.peekToken(); + if (!noParens && tok.type === lexer2.TOKEN_RIGHT_PAREN) { + this.nextToken(); + break; + } else if (noParens && tok.type === lexer2.TOKEN_BLOCK_END) { + break; + } + if (checkComma && !this.skip(lexer2.TOKEN_COMMA)) { + this.fail("parseSignature: expected comma after expression", tok.lineno, tok.colno); + } else { + var arg = this.parseExpression(); + if (this.skipValue(lexer2.TOKEN_OPERATOR, "=")) { + kwargs.addChild(new nodes2.Pair(arg.lineno, arg.colno, arg, this.parseExpression())); + } else { + args.addChild(arg); + } + } + checkComma = true; + } + if (kwargs.children.length) { + args.addChild(kwargs); + } + return args; + }; + _proto.parseUntilBlocks = function parseUntilBlocks() { + var prev = this.breakOnBlocks; + for (var _len = arguments.length, blockNames = new Array(_len), _key = 0; _key < _len; _key++) { + blockNames[_key] = arguments[_key]; + } + this.breakOnBlocks = blockNames; + var ret = this.parse(); + this.breakOnBlocks = prev; + return ret; + }; + _proto.parseNodes = function parseNodes() { + var tok; + var buf = []; + while (tok = this.nextToken()) { + if (tok.type === lexer2.TOKEN_DATA) { + var data = tok.value; + var nextToken = this.peekToken(); + var nextVal = nextToken && nextToken.value; + if (this.dropLeadingWhitespace) { + data = data.replace(/^\s*/, ""); + this.dropLeadingWhitespace = false; + } + if (nextToken && (nextToken.type === lexer2.TOKEN_BLOCK_START && nextVal.charAt(nextVal.length - 1) === "-" || nextToken.type === lexer2.TOKEN_VARIABLE_START && nextVal.charAt(this.tokens.tags.VARIABLE_START.length) === "-" || nextToken.type === lexer2.TOKEN_COMMENT && nextVal.charAt(this.tokens.tags.COMMENT_START.length) === "-")) { + data = data.replace(/\s*$/, ""); + } + buf.push(new nodes2.Output(tok.lineno, tok.colno, [new nodes2.TemplateData(tok.lineno, tok.colno, data)])); + } else if (tok.type === lexer2.TOKEN_BLOCK_START) { + this.dropLeadingWhitespace = false; + var n = this.parseStatement(); + if (!n) { + break; + } + buf.push(n); + } else if (tok.type === lexer2.TOKEN_VARIABLE_START) { + var e2 = this.parseExpression(); + this.dropLeadingWhitespace = false; + this.advanceAfterVariableEnd(); + buf.push(new nodes2.Output(tok.lineno, tok.colno, [e2])); + } else if (tok.type === lexer2.TOKEN_COMMENT) { + this.dropLeadingWhitespace = tok.value.charAt(tok.value.length - this.tokens.tags.COMMENT_END.length - 1) === "-"; + } else { + this.fail("Unexpected token at top-level: " + tok.type, tok.lineno, tok.colno); + } + } + return buf; + }; + _proto.parse = function parse() { + return new nodes2.NodeList(0, 0, this.parseNodes()); + }; + _proto.parseAsRoot = function parseAsRoot() { + return new nodes2.Root(0, 0, this.parseNodes()); + }; + return Parser2; + }(Obj); + module2.exports = { + parse: function parse(src, extensions, opts) { + var p = new Parser(lexer2.lex(src, opts)); + if (extensions !== void 0) { + p.extensions = extensions; + } + return p.parseAsRoot(); + }, + Parser + }; +}); + +// ../../node_modules/nunjucks/src/transformer.js +var require_transformer = __commonJS((exports2, module2) => { + "use strict"; + var nodes2 = require_nodes(); + var lib2 = require_lib(); + var sym = 0; + function gensym() { + return "hole_" + sym++; + } + function mapCOW(arr, func) { + var res = null; + for (var i = 0; i < arr.length; i++) { + var item = func(arr[i]); + if (item !== arr[i]) { + if (!res) { + res = arr.slice(); + } + res[i] = item; + } + } + return res || arr; + } + function walk(ast, func, depthFirst) { + if (!(ast instanceof nodes2.Node)) { + return ast; + } + if (!depthFirst) { + var astT = func(ast); + if (astT && astT !== ast) { + return astT; + } + } + if (ast instanceof nodes2.NodeList) { + var children = mapCOW(ast.children, function(node) { + return walk(node, func, depthFirst); + }); + if (children !== ast.children) { + ast = new nodes2[ast.typename](ast.lineno, ast.colno, children); + } + } else if (ast instanceof nodes2.CallExtension) { + var args = walk(ast.args, func, depthFirst); + var contentArgs = mapCOW(ast.contentArgs, function(node) { + return walk(node, func, depthFirst); + }); + if (args !== ast.args || contentArgs !== ast.contentArgs) { + ast = new nodes2[ast.typename](ast.extName, ast.prop, args, contentArgs); + } + } else { + var props = ast.fields.map(function(field) { + return ast[field]; + }); + var propsT = mapCOW(props, function(prop) { + return walk(prop, func, depthFirst); + }); + if (propsT !== props) { + ast = new nodes2[ast.typename](ast.lineno, ast.colno); + propsT.forEach(function(prop, i) { + ast[ast.fields[i]] = prop; + }); + } + } + return depthFirst ? func(ast) || ast : ast; + } + function depthWalk(ast, func) { + return walk(ast, func, true); + } + function _liftFilters(node, asyncFilters, prop) { + var children = []; + var walked = depthWalk(prop ? node[prop] : node, function(descNode) { + var symbol; + if (descNode instanceof nodes2.Block) { + return descNode; + } else if (descNode instanceof nodes2.Filter && lib2.indexOf(asyncFilters, descNode.name.value) !== -1 || descNode instanceof nodes2.CallExtensionAsync) { + symbol = new nodes2.Symbol(descNode.lineno, descNode.colno, gensym()); + children.push(new nodes2.FilterAsync(descNode.lineno, descNode.colno, descNode.name, descNode.args, symbol)); + } + return symbol; + }); + if (prop) { + node[prop] = walked; + } else { + node = walked; + } + if (children.length) { + children.push(node); + return new nodes2.NodeList(node.lineno, node.colno, children); + } else { + return node; + } + } + function liftFilters(ast, asyncFilters) { + return depthWalk(ast, function(node) { + if (node instanceof nodes2.Output) { + return _liftFilters(node, asyncFilters); + } else if (node instanceof nodes2.Set) { + return _liftFilters(node, asyncFilters, "value"); + } else if (node instanceof nodes2.For) { + return _liftFilters(node, asyncFilters, "arr"); + } else if (node instanceof nodes2.If) { + return _liftFilters(node, asyncFilters, "cond"); + } else if (node instanceof nodes2.CallExtension) { + return _liftFilters(node, asyncFilters, "args"); + } else { + return void 0; + } + }); + } + function liftSuper(ast) { + return walk(ast, function(blockNode) { + if (!(blockNode instanceof nodes2.Block)) { + return; + } + var hasSuper = false; + var symbol = gensym(); + blockNode.body = walk(blockNode.body, function(node) { + if (node instanceof nodes2.FunCall && node.name.value === "super") { + hasSuper = true; + return new nodes2.Symbol(node.lineno, node.colno, symbol); + } + }); + if (hasSuper) { + blockNode.body.children.unshift(new nodes2.Super(0, 0, blockNode.name, new nodes2.Symbol(0, 0, symbol))); + } + }); + } + function convertStatements(ast) { + return depthWalk(ast, function(node) { + if (!(node instanceof nodes2.If) && !(node instanceof nodes2.For)) { + return void 0; + } + var async = false; + walk(node, function(child) { + if (child instanceof nodes2.FilterAsync || child instanceof nodes2.IfAsync || child instanceof nodes2.AsyncEach || child instanceof nodes2.AsyncAll || child instanceof nodes2.CallExtensionAsync) { + async = true; + return child; + } + return void 0; + }); + if (async) { + if (node instanceof nodes2.If) { + return new nodes2.IfAsync(node.lineno, node.colno, node.cond, node.body, node.else_); + } else if (node instanceof nodes2.For && !(node instanceof nodes2.AsyncAll)) { + return new nodes2.AsyncEach(node.lineno, node.colno, node.arr, node.name, node.body, node.else_); + } + } + return void 0; + }); + } + function cps(ast, asyncFilters) { + return convertStatements(liftSuper(liftFilters(ast, asyncFilters))); + } + function transform(ast, asyncFilters) { + return cps(ast, asyncFilters || []); + } + module2.exports = { + transform + }; +}); + +// ../../node_modules/nunjucks/src/runtime.js +var require_runtime = __commonJS((exports2, module2) => { + "use strict"; + var lib2 = require_lib(); + var arrayFrom = Array.from; + var supportsIterators = typeof Symbol === "function" && Symbol.iterator && typeof arrayFrom === "function"; + var Frame = /* @__PURE__ */ function() { + function Frame2(parent, isolateWrites) { + this.variables = Object.create(null); + this.parent = parent; + this.topLevel = false; + this.isolateWrites = isolateWrites; + } + var _proto = Frame2.prototype; + _proto.set = function set(name, val, resolveUp) { + var parts = name.split("."); + var obj = this.variables; + var frame = this; + if (resolveUp) { + if (frame = this.resolve(parts[0], true)) { + frame.set(name, val); + return; + } + } + for (var i = 0; i < parts.length - 1; i++) { + var id = parts[i]; + if (!obj[id]) { + obj[id] = {}; + } + obj = obj[id]; + } + obj[parts[parts.length - 1]] = val; + }; + _proto.get = function get(name) { + var val = this.variables[name]; + if (val !== void 0) { + return val; + } + return null; + }; + _proto.lookup = function lookup(name) { + var p = this.parent; + var val = this.variables[name]; + if (val !== void 0) { + return val; + } + return p && p.lookup(name); + }; + _proto.resolve = function resolve(name, forWrite) { + var p = forWrite && this.isolateWrites ? void 0 : this.parent; + var val = this.variables[name]; + if (val !== void 0) { + return this; + } + return p && p.resolve(name); + }; + _proto.push = function push(isolateWrites) { + return new Frame2(this, isolateWrites); + }; + _proto.pop = function pop() { + return this.parent; + }; + return Frame2; + }(); + function makeMacro(argNames, kwargNames, func) { + return function macro() { + for (var _len = arguments.length, macroArgs = new Array(_len), _key = 0; _key < _len; _key++) { + macroArgs[_key] = arguments[_key]; + } + var argCount = numArgs(macroArgs); + var args; + var kwargs = getKeywordArgs(macroArgs); + if (argCount > argNames.length) { + args = macroArgs.slice(0, argNames.length); + macroArgs.slice(args.length, argCount).forEach(function(val, i2) { + if (i2 < kwargNames.length) { + kwargs[kwargNames[i2]] = val; + } + }); + args.push(kwargs); + } else if (argCount < argNames.length) { + args = macroArgs.slice(0, argCount); + for (var i = argCount; i < argNames.length; i++) { + var arg = argNames[i]; + args.push(kwargs[arg]); + delete kwargs[arg]; + } + args.push(kwargs); + } else { + args = macroArgs; + } + return func.apply(this, args); + }; + } + function makeKeywordArgs(obj) { + obj.__keywords = true; + return obj; + } + function isKeywordArgs(obj) { + return obj && Object.prototype.hasOwnProperty.call(obj, "__keywords"); + } + function getKeywordArgs(args) { + var len = args.length; + if (len) { + var lastArg = args[len - 1]; + if (isKeywordArgs(lastArg)) { + return lastArg; + } + } + return {}; + } + function numArgs(args) { + var len = args.length; + if (len === 0) { + return 0; + } + var lastArg = args[len - 1]; + if (isKeywordArgs(lastArg)) { + return len - 1; + } else { + return len; + } + } + function SafeString(val) { + if (typeof val !== "string") { + return val; + } + this.val = val; + this.length = val.length; + } + SafeString.prototype = Object.create(String.prototype, { + length: { + writable: true, + configurable: true, + value: 0 + } + }); + SafeString.prototype.valueOf = function valueOf() { + return this.val; + }; + SafeString.prototype.toString = function toString() { + return this.val; + }; + function copySafeness(dest, target) { + if (dest instanceof SafeString) { + return new SafeString(target); + } + return target.toString(); + } + function markSafe(val) { + var type = typeof val; + if (type === "string") { + return new SafeString(val); + } else if (type !== "function") { + return val; + } else { + return function wrapSafe(args) { + var ret = val.apply(this, arguments); + if (typeof ret === "string") { + return new SafeString(ret); + } + return ret; + }; + } + } + function suppressValue(val, autoescape) { + val = val !== void 0 && val !== null ? val : ""; + if (autoescape && !(val instanceof SafeString)) { + val = lib2.escape(val.toString()); + } + return val; + } + function ensureDefined(val, lineno, colno) { + if (val === null || val === void 0) { + throw new lib2.TemplateError("attempted to output null or undefined value", lineno + 1, colno + 1); + } + return val; + } + function memberLookup(obj, val) { + if (obj === void 0 || obj === null) { + return void 0; + } + if (typeof obj[val] === "function") { + return function() { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + return obj[val].apply(obj, args); + }; + } + return obj[val]; + } + function callWrap(obj, name, context, args) { + if (!obj) { + throw new Error("Unable to call `" + name + "`, which is undefined or falsey"); + } else if (typeof obj !== "function") { + throw new Error("Unable to call `" + name + "`, which is not a function"); + } + return obj.apply(context, args); + } + function contextOrFrameLookup(context, frame, name) { + var val = frame.lookup(name); + return val !== void 0 ? val : context.lookup(name); + } + function handleError(error, lineno, colno) { + if (error.lineno) { + return error; + } else { + return new lib2.TemplateError(error, lineno, colno); + } + } + function asyncEach(arr, dimen, iter, cb) { + if (lib2.isArray(arr)) { + var len = arr.length; + lib2.asyncIter(arr, function iterCallback(item, i, next) { + switch (dimen) { + case 1: + iter(item, i, len, next); + break; + case 2: + iter(item[0], item[1], i, len, next); + break; + case 3: + iter(item[0], item[1], item[2], i, len, next); + break; + default: + item.push(i, len, next); + iter.apply(this, item); + } + }, cb); + } else { + lib2.asyncFor(arr, function iterCallback(key, val, i, len2, next) { + iter(key, val, i, len2, next); + }, cb); + } + } + function asyncAll(arr, dimen, func, cb) { + var finished = 0; + var len; + var outputArr; + function done(i2, output) { + finished++; + outputArr[i2] = output; + if (finished === len) { + cb(null, outputArr.join("")); + } + } + if (lib2.isArray(arr)) { + len = arr.length; + outputArr = new Array(len); + if (len === 0) { + cb(null, ""); + } else { + for (var i = 0; i < arr.length; i++) { + var item = arr[i]; + switch (dimen) { + case 1: + func(item, i, len, done); + break; + case 2: + func(item[0], item[1], i, len, done); + break; + case 3: + func(item[0], item[1], item[2], i, len, done); + break; + default: + item.push(i, len, done); + func.apply(this, item); + } + } + } + } else { + var keys = lib2.keys(arr || {}); + len = keys.length; + outputArr = new Array(len); + if (len === 0) { + cb(null, ""); + } else { + for (var _i = 0; _i < keys.length; _i++) { + var k = keys[_i]; + func(k, arr[k], _i, len, done); + } + } + } + } + function fromIterator(arr) { + if (typeof arr !== "object" || arr === null || lib2.isArray(arr)) { + return arr; + } else if (supportsIterators && Symbol.iterator in arr) { + return arrayFrom(arr); + } else { + return arr; + } + } + module2.exports = { + Frame, + makeMacro, + makeKeywordArgs, + numArgs, + suppressValue, + ensureDefined, + memberLookup, + contextOrFrameLookup, + callWrap, + handleError, + isArray: lib2.isArray, + keys: lib2.keys, + SafeString, + copySafeness, + markSafe, + asyncEach, + asyncAll, + inOperator: lib2.inOperator, + fromIterator + }; +}); + +// ../../node_modules/nunjucks/src/compiler.js +var require_compiler = __commonJS((exports2, module2) => { + "use strict"; + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var parser2 = require_parser(); + var transformer = require_transformer(); + var nodes2 = require_nodes(); + var _require2 = require_lib(); + var TemplateError = _require2.TemplateError; + var _require22 = require_runtime(); + var Frame = _require22.Frame; + var _require3 = require_object(); + var Obj = _require3.Obj; + var compareOps = { + "==": "==", + "===": "===", + "!=": "!=", + "!==": "!==", + "<": "<", + ">": ">", + "<=": "<=", + ">=": ">=" + }; + var Compiler = /* @__PURE__ */ function(_Obj) { + _inheritsLoose(Compiler2, _Obj); + function Compiler2() { + return _Obj.apply(this, arguments) || this; + } + var _proto = Compiler2.prototype; + _proto.init = function init(templateName, throwOnUndefined) { + this.templateName = templateName; + this.codebuf = []; + this.lastId = 0; + this.buffer = null; + this.bufferStack = []; + this._scopeClosers = ""; + this.inBlock = false; + this.throwOnUndefined = throwOnUndefined; + }; + _proto.fail = function fail(msg, lineno, colno) { + if (lineno !== void 0) { + lineno += 1; + } + if (colno !== void 0) { + colno += 1; + } + throw new TemplateError(msg, lineno, colno); + }; + _proto._pushBuffer = function _pushBuffer() { + var id = this._tmpid(); + this.bufferStack.push(this.buffer); + this.buffer = id; + this._emit("var " + this.buffer + ' = "";'); + return id; + }; + _proto._popBuffer = function _popBuffer() { + this.buffer = this.bufferStack.pop(); + }; + _proto._emit = function _emit(code) { + this.codebuf.push(code); + }; + _proto._emitLine = function _emitLine(code) { + this._emit(code + "\n"); + }; + _proto._emitLines = function _emitLines() { + var _this = this; + for (var _len = arguments.length, lines = new Array(_len), _key = 0; _key < _len; _key++) { + lines[_key] = arguments[_key]; + } + lines.forEach(function(line) { + return _this._emitLine(line); + }); + }; + _proto._emitFuncBegin = function _emitFuncBegin(node, name) { + this.buffer = "output"; + this._scopeClosers = ""; + this._emitLine("function " + name + "(env, context, frame, runtime, cb) {"); + this._emitLine("var lineno = " + node.lineno + ";"); + this._emitLine("var colno = " + node.colno + ";"); + this._emitLine("var " + this.buffer + ' = "";'); + this._emitLine("try {"); + }; + _proto._emitFuncEnd = function _emitFuncEnd(noReturn) { + if (!noReturn) { + this._emitLine("cb(null, " + this.buffer + ");"); + } + this._closeScopeLevels(); + this._emitLine("} catch (e) {"); + this._emitLine(" cb(runtime.handleError(e, lineno, colno));"); + this._emitLine("}"); + this._emitLine("}"); + this.buffer = null; + }; + _proto._addScopeLevel = function _addScopeLevel() { + this._scopeClosers += "})"; + }; + _proto._closeScopeLevels = function _closeScopeLevels() { + this._emitLine(this._scopeClosers + ";"); + this._scopeClosers = ""; + }; + _proto._withScopedSyntax = function _withScopedSyntax(func) { + var _scopeClosers = this._scopeClosers; + this._scopeClosers = ""; + func.call(this); + this._closeScopeLevels(); + this._scopeClosers = _scopeClosers; + }; + _proto._makeCallback = function _makeCallback(res) { + var err = this._tmpid(); + return "function(" + err + (res ? "," + res : "") + ") {\nif(" + err + ") { cb(" + err + "); return; }"; + }; + _proto._tmpid = function _tmpid() { + this.lastId++; + return "t_" + this.lastId; + }; + _proto._templateName = function _templateName() { + return this.templateName == null ? "undefined" : JSON.stringify(this.templateName); + }; + _proto._compileChildren = function _compileChildren(node, frame) { + var _this2 = this; + node.children.forEach(function(child) { + _this2.compile(child, frame); + }); + }; + _proto._compileAggregate = function _compileAggregate(node, frame, startChar, endChar) { + var _this3 = this; + if (startChar) { + this._emit(startChar); + } + node.children.forEach(function(child, i) { + if (i > 0) { + _this3._emit(","); + } + _this3.compile(child, frame); + }); + if (endChar) { + this._emit(endChar); + } + }; + _proto._compileExpression = function _compileExpression(node, frame) { + this.assertType(node, nodes2.Literal, nodes2.Symbol, nodes2.Group, nodes2.Array, nodes2.Dict, nodes2.FunCall, nodes2.Caller, nodes2.Filter, nodes2.LookupVal, nodes2.Compare, nodes2.InlineIf, nodes2.In, nodes2.Is, nodes2.And, nodes2.Or, nodes2.Not, nodes2.Add, nodes2.Concat, nodes2.Sub, nodes2.Mul, nodes2.Div, nodes2.FloorDiv, nodes2.Mod, nodes2.Pow, nodes2.Neg, nodes2.Pos, nodes2.Compare, nodes2.NodeList); + this.compile(node, frame); + }; + _proto.assertType = function assertType(node) { + for (var _len2 = arguments.length, types = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + types[_key2 - 1] = arguments[_key2]; + } + if (!types.some(function(t) { + return node instanceof t; + })) { + this.fail("assertType: invalid type: " + node.typename, node.lineno, node.colno); + } + }; + _proto.compileCallExtension = function compileCallExtension(node, frame, async) { + var _this4 = this; + var args = node.args; + var contentArgs = node.contentArgs; + var autoescape = typeof node.autoescape === "boolean" ? node.autoescape : true; + if (!async) { + this._emit(this.buffer + " += runtime.suppressValue("); + } + this._emit('env.getExtension("' + node.extName + '")["' + node.prop + '"]('); + this._emit("context"); + if (args || contentArgs) { + this._emit(","); + } + if (args) { + if (!(args instanceof nodes2.NodeList)) { + this.fail("compileCallExtension: arguments must be a NodeList, use `parser.parseSignature`"); + } + args.children.forEach(function(arg, i) { + _this4._compileExpression(arg, frame); + if (i !== args.children.length - 1 || contentArgs.length) { + _this4._emit(","); + } + }); + } + if (contentArgs.length) { + contentArgs.forEach(function(arg, i) { + if (i > 0) { + _this4._emit(","); + } + if (arg) { + _this4._emitLine("function(cb) {"); + _this4._emitLine("if(!cb) { cb = function(err) { if(err) { throw err; }}}"); + var id = _this4._pushBuffer(); + _this4._withScopedSyntax(function() { + _this4.compile(arg, frame); + _this4._emitLine("cb(null, " + id + ");"); + }); + _this4._popBuffer(); + _this4._emitLine("return " + id + ";"); + _this4._emitLine("}"); + } else { + _this4._emit("null"); + } + }); + } + if (async) { + var res = this._tmpid(); + this._emitLine(", " + this._makeCallback(res)); + this._emitLine(this.buffer + " += runtime.suppressValue(" + res + ", " + autoescape + " && env.opts.autoescape);"); + this._addScopeLevel(); + } else { + this._emit(")"); + this._emit(", " + autoescape + " && env.opts.autoescape);\n"); + } + }; + _proto.compileCallExtensionAsync = function compileCallExtensionAsync(node, frame) { + this.compileCallExtension(node, frame, true); + }; + _proto.compileNodeList = function compileNodeList(node, frame) { + this._compileChildren(node, frame); + }; + _proto.compileLiteral = function compileLiteral(node) { + if (typeof node.value === "string") { + var val = node.value.replace(/\\/g, "\\\\"); + val = val.replace(/"/g, '\\"'); + val = val.replace(/\n/g, "\\n"); + val = val.replace(/\r/g, "\\r"); + val = val.replace(/\t/g, "\\t"); + val = val.replace(/\u2028/g, "\\u2028"); + this._emit('"' + val + '"'); + } else if (node.value === null) { + this._emit("null"); + } else { + this._emit(node.value.toString()); + } + }; + _proto.compileSymbol = function compileSymbol(node, frame) { + var name = node.value; + var v = frame.lookup(name); + if (v) { + this._emit(v); + } else { + this._emit('runtime.contextOrFrameLookup(context, frame, "' + name + '")'); + } + }; + _proto.compileGroup = function compileGroup(node, frame) { + this._compileAggregate(node, frame, "(", ")"); + }; + _proto.compileArray = function compileArray(node, frame) { + this._compileAggregate(node, frame, "[", "]"); + }; + _proto.compileDict = function compileDict(node, frame) { + this._compileAggregate(node, frame, "{", "}"); + }; + _proto.compilePair = function compilePair(node, frame) { + var key = node.key; + var val = node.value; + if (key instanceof nodes2.Symbol) { + key = new nodes2.Literal(key.lineno, key.colno, key.value); + } else if (!(key instanceof nodes2.Literal && typeof key.value === "string")) { + this.fail("compilePair: Dict keys must be strings or names", key.lineno, key.colno); + } + this.compile(key, frame); + this._emit(": "); + this._compileExpression(val, frame); + }; + _proto.compileInlineIf = function compileInlineIf(node, frame) { + this._emit("("); + this.compile(node.cond, frame); + this._emit("?"); + this.compile(node.body, frame); + this._emit(":"); + if (node.else_ !== null) { + this.compile(node.else_, frame); + } else { + this._emit('""'); + } + this._emit(")"); + }; + _proto.compileIn = function compileIn(node, frame) { + this._emit("runtime.inOperator("); + this.compile(node.left, frame); + this._emit(","); + this.compile(node.right, frame); + this._emit(")"); + }; + _proto.compileIs = function compileIs(node, frame) { + var right = node.right.name ? node.right.name.value : node.right.value; + this._emit('env.getTest("' + right + '").call(context, '); + this.compile(node.left, frame); + if (node.right.args) { + this._emit(","); + this.compile(node.right.args, frame); + } + this._emit(") === true"); + }; + _proto._binOpEmitter = function _binOpEmitter(node, frame, str) { + this.compile(node.left, frame); + this._emit(str); + this.compile(node.right, frame); + }; + _proto.compileOr = function compileOr(node, frame) { + return this._binOpEmitter(node, frame, " || "); + }; + _proto.compileAnd = function compileAnd(node, frame) { + return this._binOpEmitter(node, frame, " && "); + }; + _proto.compileAdd = function compileAdd(node, frame) { + return this._binOpEmitter(node, frame, " + "); + }; + _proto.compileConcat = function compileConcat(node, frame) { + return this._binOpEmitter(node, frame, ' + "" + '); + }; + _proto.compileSub = function compileSub(node, frame) { + return this._binOpEmitter(node, frame, " - "); + }; + _proto.compileMul = function compileMul(node, frame) { + return this._binOpEmitter(node, frame, " * "); + }; + _proto.compileDiv = function compileDiv(node, frame) { + return this._binOpEmitter(node, frame, " / "); + }; + _proto.compileMod = function compileMod(node, frame) { + return this._binOpEmitter(node, frame, " % "); + }; + _proto.compileNot = function compileNot(node, frame) { + this._emit("!"); + this.compile(node.target, frame); + }; + _proto.compileFloorDiv = function compileFloorDiv(node, frame) { + this._emit("Math.floor("); + this.compile(node.left, frame); + this._emit(" / "); + this.compile(node.right, frame); + this._emit(")"); + }; + _proto.compilePow = function compilePow(node, frame) { + this._emit("Math.pow("); + this.compile(node.left, frame); + this._emit(", "); + this.compile(node.right, frame); + this._emit(")"); + }; + _proto.compileNeg = function compileNeg(node, frame) { + this._emit("-"); + this.compile(node.target, frame); + }; + _proto.compilePos = function compilePos(node, frame) { + this._emit("+"); + this.compile(node.target, frame); + }; + _proto.compileCompare = function compileCompare(node, frame) { + var _this5 = this; + this.compile(node.expr, frame); + node.ops.forEach(function(op) { + _this5._emit(" " + compareOps[op.type] + " "); + _this5.compile(op.expr, frame); + }); + }; + _proto.compileLookupVal = function compileLookupVal(node, frame) { + this._emit("runtime.memberLookup(("); + this._compileExpression(node.target, frame); + this._emit("),"); + this._compileExpression(node.val, frame); + this._emit(")"); + }; + _proto._getNodeName = function _getNodeName(node) { + switch (node.typename) { + case "Symbol": + return node.value; + case "FunCall": + return "the return value of (" + this._getNodeName(node.name) + ")"; + case "LookupVal": + return this._getNodeName(node.target) + '["' + this._getNodeName(node.val) + '"]'; + case "Literal": + return node.value.toString(); + default: + return "--expression--"; + } + }; + _proto.compileFunCall = function compileFunCall(node, frame) { + this._emit("(lineno = " + node.lineno + ", colno = " + node.colno + ", "); + this._emit("runtime.callWrap("); + this._compileExpression(node.name, frame); + this._emit(', "' + this._getNodeName(node.name).replace(/"/g, '\\"') + '", context, '); + this._compileAggregate(node.args, frame, "[", "])"); + this._emit(")"); + }; + _proto.compileFilter = function compileFilter(node, frame) { + var name = node.name; + this.assertType(name, nodes2.Symbol); + this._emit('env.getFilter("' + name.value + '").call(context, '); + this._compileAggregate(node.args, frame); + this._emit(")"); + }; + _proto.compileFilterAsync = function compileFilterAsync(node, frame) { + var name = node.name; + var symbol = node.symbol.value; + this.assertType(name, nodes2.Symbol); + frame.set(symbol, symbol); + this._emit('env.getFilter("' + name.value + '").call(context, '); + this._compileAggregate(node.args, frame); + this._emitLine(", " + this._makeCallback(symbol)); + this._addScopeLevel(); + }; + _proto.compileKeywordArgs = function compileKeywordArgs(node, frame) { + this._emit("runtime.makeKeywordArgs("); + this.compileDict(node, frame); + this._emit(")"); + }; + _proto.compileSet = function compileSet(node, frame) { + var _this6 = this; + var ids = []; + node.targets.forEach(function(target) { + var name = target.value; + var id = frame.lookup(name); + if (id === null || id === void 0) { + id = _this6._tmpid(); + _this6._emitLine("var " + id + ";"); + } + ids.push(id); + }); + if (node.value) { + this._emit(ids.join(" = ") + " = "); + this._compileExpression(node.value, frame); + this._emitLine(";"); + } else { + this._emit(ids.join(" = ") + " = "); + this.compile(node.body, frame); + this._emitLine(";"); + } + node.targets.forEach(function(target, i) { + var id = ids[i]; + var name = target.value; + _this6._emitLine('frame.set("' + name + '", ' + id + ", true);"); + _this6._emitLine("if(frame.topLevel) {"); + _this6._emitLine('context.setVariable("' + name + '", ' + id + ");"); + _this6._emitLine("}"); + if (name.charAt(0) !== "_") { + _this6._emitLine("if(frame.topLevel) {"); + _this6._emitLine('context.addExport("' + name + '", ' + id + ");"); + _this6._emitLine("}"); + } + }); + }; + _proto.compileSwitch = function compileSwitch(node, frame) { + var _this7 = this; + this._emit("switch ("); + this.compile(node.expr, frame); + this._emit(") {"); + node.cases.forEach(function(c, i) { + _this7._emit("case "); + _this7.compile(c.cond, frame); + _this7._emit(": "); + _this7.compile(c.body, frame); + if (c.body.children.length) { + _this7._emitLine("break;"); + } + }); + if (node.default) { + this._emit("default:"); + this.compile(node.default, frame); + } + this._emit("}"); + }; + _proto.compileIf = function compileIf(node, frame, async) { + var _this8 = this; + this._emit("if("); + this._compileExpression(node.cond, frame); + this._emitLine(") {"); + this._withScopedSyntax(function() { + _this8.compile(node.body, frame); + if (async) { + _this8._emit("cb()"); + } + }); + if (node.else_) { + this._emitLine("}\nelse {"); + this._withScopedSyntax(function() { + _this8.compile(node.else_, frame); + if (async) { + _this8._emit("cb()"); + } + }); + } else if (async) { + this._emitLine("}\nelse {"); + this._emit("cb()"); + } + this._emitLine("}"); + }; + _proto.compileIfAsync = function compileIfAsync(node, frame) { + this._emit("(function(cb) {"); + this.compileIf(node, frame, true); + this._emit("})(" + this._makeCallback()); + this._addScopeLevel(); + }; + _proto._emitLoopBindings = function _emitLoopBindings(node, arr, i, len) { + var _this9 = this; + var bindings = [{ + name: "index", + val: i + " + 1" + }, { + name: "index0", + val: i + }, { + name: "revindex", + val: len + " - " + i + }, { + name: "revindex0", + val: len + " - " + i + " - 1" + }, { + name: "first", + val: i + " === 0" + }, { + name: "last", + val: i + " === " + len + " - 1" + }, { + name: "length", + val: len + }]; + bindings.forEach(function(b) { + _this9._emitLine('frame.set("loop.' + b.name + '", ' + b.val + ");"); + }); + }; + _proto.compileFor = function compileFor(node, frame) { + var _this10 = this; + var i = this._tmpid(); + var len = this._tmpid(); + var arr = this._tmpid(); + frame = frame.push(); + this._emitLine("frame = frame.push();"); + this._emit("var " + arr + " = "); + this._compileExpression(node.arr, frame); + this._emitLine(";"); + this._emit("if(" + arr + ") {"); + this._emitLine(arr + " = runtime.fromIterator(" + arr + ");"); + if (node.name instanceof nodes2.Array) { + this._emitLine("var " + i + ";"); + this._emitLine("if(runtime.isArray(" + arr + ")) {"); + this._emitLine("var " + len + " = " + arr + ".length;"); + this._emitLine("for(" + i + "=0; " + i + " < " + arr + ".length; " + i + "++) {"); + node.name.children.forEach(function(child, u) { + var tid = _this10._tmpid(); + _this10._emitLine("var " + tid + " = " + arr + "[" + i + "][" + u + "];"); + _this10._emitLine('frame.set("' + child + '", ' + arr + "[" + i + "][" + u + "]);"); + frame.set(node.name.children[u].value, tid); + }); + this._emitLoopBindings(node, arr, i, len); + this._withScopedSyntax(function() { + _this10.compile(node.body, frame); + }); + this._emitLine("}"); + this._emitLine("} else {"); + var _node$name$children = node.name.children, key = _node$name$children[0], val = _node$name$children[1]; + var k = this._tmpid(); + var v = this._tmpid(); + frame.set(key.value, k); + frame.set(val.value, v); + this._emitLine(i + " = -1;"); + this._emitLine("var " + len + " = runtime.keys(" + arr + ").length;"); + this._emitLine("for(var " + k + " in " + arr + ") {"); + this._emitLine(i + "++;"); + this._emitLine("var " + v + " = " + arr + "[" + k + "];"); + this._emitLine('frame.set("' + key.value + '", ' + k + ");"); + this._emitLine('frame.set("' + val.value + '", ' + v + ");"); + this._emitLoopBindings(node, arr, i, len); + this._withScopedSyntax(function() { + _this10.compile(node.body, frame); + }); + this._emitLine("}"); + this._emitLine("}"); + } else { + var _v = this._tmpid(); + frame.set(node.name.value, _v); + this._emitLine("var " + len + " = " + arr + ".length;"); + this._emitLine("for(var " + i + "=0; " + i + " < " + arr + ".length; " + i + "++) {"); + this._emitLine("var " + _v + " = " + arr + "[" + i + "];"); + this._emitLine('frame.set("' + node.name.value + '", ' + _v + ");"); + this._emitLoopBindings(node, arr, i, len); + this._withScopedSyntax(function() { + _this10.compile(node.body, frame); + }); + this._emitLine("}"); + } + this._emitLine("}"); + if (node.else_) { + this._emitLine("if (!" + len + ") {"); + this.compile(node.else_, frame); + this._emitLine("}"); + } + this._emitLine("frame = frame.pop();"); + }; + _proto._compileAsyncLoop = function _compileAsyncLoop(node, frame, parallel) { + var _this11 = this; + var i = this._tmpid(); + var len = this._tmpid(); + var arr = this._tmpid(); + var asyncMethod = parallel ? "asyncAll" : "asyncEach"; + frame = frame.push(); + this._emitLine("frame = frame.push();"); + this._emit("var " + arr + " = runtime.fromIterator("); + this._compileExpression(node.arr, frame); + this._emitLine(");"); + if (node.name instanceof nodes2.Array) { + var arrayLen = node.name.children.length; + this._emit("runtime." + asyncMethod + "(" + arr + ", " + arrayLen + ", function("); + node.name.children.forEach(function(name) { + _this11._emit(name.value + ","); + }); + this._emit(i + "," + len + ",next) {"); + node.name.children.forEach(function(name) { + var id2 = name.value; + frame.set(id2, id2); + _this11._emitLine('frame.set("' + id2 + '", ' + id2 + ");"); + }); + } else { + var id = node.name.value; + this._emitLine("runtime." + asyncMethod + "(" + arr + ", 1, function(" + id + ", " + i + ", " + len + ",next) {"); + this._emitLine('frame.set("' + id + '", ' + id + ");"); + frame.set(id, id); + } + this._emitLoopBindings(node, arr, i, len); + this._withScopedSyntax(function() { + var buf; + if (parallel) { + buf = _this11._pushBuffer(); + } + _this11.compile(node.body, frame); + _this11._emitLine("next(" + i + (buf ? "," + buf : "") + ");"); + if (parallel) { + _this11._popBuffer(); + } + }); + var output = this._tmpid(); + this._emitLine("}, " + this._makeCallback(output)); + this._addScopeLevel(); + if (parallel) { + this._emitLine(this.buffer + " += " + output + ";"); + } + if (node.else_) { + this._emitLine("if (!" + arr + ".length) {"); + this.compile(node.else_, frame); + this._emitLine("}"); + } + this._emitLine("frame = frame.pop();"); + }; + _proto.compileAsyncEach = function compileAsyncEach(node, frame) { + this._compileAsyncLoop(node, frame); + }; + _proto.compileAsyncAll = function compileAsyncAll(node, frame) { + this._compileAsyncLoop(node, frame, true); + }; + _proto._compileMacro = function _compileMacro(node, frame) { + var _this12 = this; + var args = []; + var kwargs = null; + var funcId = "macro_" + this._tmpid(); + var keepFrame = frame !== void 0; + node.args.children.forEach(function(arg, i) { + if (i === node.args.children.length - 1 && arg instanceof nodes2.Dict) { + kwargs = arg; + } else { + _this12.assertType(arg, nodes2.Symbol); + args.push(arg); + } + }); + var realNames = [].concat(args.map(function(n) { + return "l_" + n.value; + }), ["kwargs"]); + var argNames = args.map(function(n) { + return '"' + n.value + '"'; + }); + var kwargNames = (kwargs && kwargs.children || []).map(function(n) { + return '"' + n.key.value + '"'; + }); + var currFrame; + if (keepFrame) { + currFrame = frame.push(true); + } else { + currFrame = new Frame(); + } + this._emitLines("var " + funcId + " = runtime.makeMacro(", "[" + argNames.join(", ") + "], ", "[" + kwargNames.join(", ") + "], ", "function (" + realNames.join(", ") + ") {", "var callerFrame = frame;", "frame = " + (keepFrame ? "frame.push(true);" : "new runtime.Frame();"), "kwargs = kwargs || {};", 'if (Object.prototype.hasOwnProperty.call(kwargs, "caller")) {', 'frame.set("caller", kwargs.caller); }'); + args.forEach(function(arg) { + _this12._emitLine('frame.set("' + arg.value + '", l_' + arg.value + ");"); + currFrame.set(arg.value, "l_" + arg.value); + }); + if (kwargs) { + kwargs.children.forEach(function(pair) { + var name = pair.key.value; + _this12._emit('frame.set("' + name + '", '); + _this12._emit('Object.prototype.hasOwnProperty.call(kwargs, "' + name + '")'); + _this12._emit(' ? kwargs["' + name + '"] : '); + _this12._compileExpression(pair.value, currFrame); + _this12._emit(");"); + }); + } + var bufferId = this._pushBuffer(); + this._withScopedSyntax(function() { + _this12.compile(node.body, currFrame); + }); + this._emitLine("frame = " + (keepFrame ? "frame.pop();" : "callerFrame;")); + this._emitLine("return new runtime.SafeString(" + bufferId + ");"); + this._emitLine("});"); + this._popBuffer(); + return funcId; + }; + _proto.compileMacro = function compileMacro(node, frame) { + var funcId = this._compileMacro(node); + var name = node.name.value; + frame.set(name, funcId); + if (frame.parent) { + this._emitLine('frame.set("' + name + '", ' + funcId + ");"); + } else { + if (node.name.value.charAt(0) !== "_") { + this._emitLine('context.addExport("' + name + '");'); + } + this._emitLine('context.setVariable("' + name + '", ' + funcId + ");"); + } + }; + _proto.compileCaller = function compileCaller(node, frame) { + this._emit("(function (){"); + var funcId = this._compileMacro(node, frame); + this._emit("return " + funcId + ";})()"); + }; + _proto._compileGetTemplate = function _compileGetTemplate(node, frame, eagerCompile, ignoreMissing) { + var parentTemplateId = this._tmpid(); + var parentName = this._templateName(); + var cb = this._makeCallback(parentTemplateId); + var eagerCompileArg = eagerCompile ? "true" : "false"; + var ignoreMissingArg = ignoreMissing ? "true" : "false"; + this._emit("env.getTemplate("); + this._compileExpression(node.template, frame); + this._emitLine(", " + eagerCompileArg + ", " + parentName + ", " + ignoreMissingArg + ", " + cb); + return parentTemplateId; + }; + _proto.compileImport = function compileImport(node, frame) { + var target = node.target.value; + var id = this._compileGetTemplate(node, frame, false, false); + this._addScopeLevel(); + this._emitLine(id + ".getExported(" + (node.withContext ? "context.getVariables(), frame, " : "") + this._makeCallback(id)); + this._addScopeLevel(); + frame.set(target, id); + if (frame.parent) { + this._emitLine('frame.set("' + target + '", ' + id + ");"); + } else { + this._emitLine('context.setVariable("' + target + '", ' + id + ");"); + } + }; + _proto.compileFromImport = function compileFromImport(node, frame) { + var _this13 = this; + var importedId = this._compileGetTemplate(node, frame, false, false); + this._addScopeLevel(); + this._emitLine(importedId + ".getExported(" + (node.withContext ? "context.getVariables(), frame, " : "") + this._makeCallback(importedId)); + this._addScopeLevel(); + node.names.children.forEach(function(nameNode) { + var name; + var alias; + var id = _this13._tmpid(); + if (nameNode instanceof nodes2.Pair) { + name = nameNode.key.value; + alias = nameNode.value.value; + } else { + name = nameNode.value; + alias = name; + } + _this13._emitLine("if(Object.prototype.hasOwnProperty.call(" + importedId + ', "' + name + '")) {'); + _this13._emitLine("var " + id + " = " + importedId + "." + name + ";"); + _this13._emitLine("} else {"); + _this13._emitLine(`cb(new Error("cannot import '` + name + `'")); return;`); + _this13._emitLine("}"); + frame.set(alias, id); + if (frame.parent) { + _this13._emitLine('frame.set("' + alias + '", ' + id + ");"); + } else { + _this13._emitLine('context.setVariable("' + alias + '", ' + id + ");"); + } + }); + }; + _proto.compileBlock = function compileBlock(node) { + var id = this._tmpid(); + if (!this.inBlock) { + this._emit('(parentTemplate ? function(e, c, f, r, cb) { cb(""); } : '); + } + this._emit('context.getBlock("' + node.name.value + '")'); + if (!this.inBlock) { + this._emit(")"); + } + this._emitLine("(env, context, frame, runtime, " + this._makeCallback(id)); + this._emitLine(this.buffer + " += " + id + ";"); + this._addScopeLevel(); + }; + _proto.compileSuper = function compileSuper(node, frame) { + var name = node.blockName.value; + var id = node.symbol.value; + var cb = this._makeCallback(id); + this._emitLine('context.getSuper(env, "' + name + '", b_' + name + ", frame, runtime, " + cb); + this._emitLine(id + " = runtime.markSafe(" + id + ");"); + this._addScopeLevel(); + frame.set(id, id); + }; + _proto.compileExtends = function compileExtends(node, frame) { + var k = this._tmpid(); + var parentTemplateId = this._compileGetTemplate(node, frame, true, false); + this._emitLine("parentTemplate = " + parentTemplateId); + this._emitLine("for(var " + k + " in parentTemplate.blocks) {"); + this._emitLine("context.addBlock(" + k + ", parentTemplate.blocks[" + k + "]);"); + this._emitLine("}"); + this._addScopeLevel(); + }; + _proto.compileInclude = function compileInclude(node, frame) { + this._emitLine("var tasks = [];"); + this._emitLine("tasks.push("); + this._emitLine("function(callback) {"); + var id = this._compileGetTemplate(node, frame, false, node.ignoreMissing); + this._emitLine("callback(null," + id + ");});"); + this._emitLine("});"); + var id2 = this._tmpid(); + this._emitLine("tasks.push("); + this._emitLine("function(template, callback){"); + this._emitLine("template.render(context.getVariables(), frame, " + this._makeCallback(id2)); + this._emitLine("callback(null," + id2 + ");});"); + this._emitLine("});"); + this._emitLine("tasks.push("); + this._emitLine("function(result, callback){"); + this._emitLine(this.buffer + " += result;"); + this._emitLine("callback(null);"); + this._emitLine("});"); + this._emitLine("env.waterfall(tasks, function(){"); + this._addScopeLevel(); + }; + _proto.compileTemplateData = function compileTemplateData(node, frame) { + this.compileLiteral(node, frame); + }; + _proto.compileCapture = function compileCapture(node, frame) { + var _this14 = this; + var buffer = this.buffer; + this.buffer = "output"; + this._emitLine("(function() {"); + this._emitLine('var output = "";'); + this._withScopedSyntax(function() { + _this14.compile(node.body, frame); + }); + this._emitLine("return output;"); + this._emitLine("})()"); + this.buffer = buffer; + }; + _proto.compileOutput = function compileOutput(node, frame) { + var _this15 = this; + var children = node.children; + children.forEach(function(child) { + if (child instanceof nodes2.TemplateData) { + if (child.value) { + _this15._emit(_this15.buffer + " += "); + _this15.compileLiteral(child, frame); + _this15._emitLine(";"); + } + } else { + _this15._emit(_this15.buffer + " += runtime.suppressValue("); + if (_this15.throwOnUndefined) { + _this15._emit("runtime.ensureDefined("); + } + _this15.compile(child, frame); + if (_this15.throwOnUndefined) { + _this15._emit("," + node.lineno + "," + node.colno + ")"); + } + _this15._emit(", env.opts.autoescape);\n"); + } + }); + }; + _proto.compileRoot = function compileRoot(node, frame) { + var _this16 = this; + if (frame) { + this.fail("compileRoot: root node can't have frame"); + } + frame = new Frame(); + this._emitFuncBegin(node, "root"); + this._emitLine("var parentTemplate = null;"); + this._compileChildren(node, frame); + this._emitLine("if(parentTemplate) {"); + this._emitLine("parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);"); + this._emitLine("} else {"); + this._emitLine("cb(null, " + this.buffer + ");"); + this._emitLine("}"); + this._emitFuncEnd(true); + this.inBlock = true; + var blockNames = []; + var blocks = node.findAll(nodes2.Block); + blocks.forEach(function(block, i) { + var name = block.name.value; + if (blockNames.indexOf(name) !== -1) { + throw new Error('Block "' + name + '" defined more than once.'); + } + blockNames.push(name); + _this16._emitFuncBegin(block, "b_" + name); + var tmpFrame = new Frame(); + _this16._emitLine("var frame = frame.push(true);"); + _this16.compile(block.body, tmpFrame); + _this16._emitFuncEnd(); + }); + this._emitLine("return {"); + blocks.forEach(function(block, i) { + var blockName = "b_" + block.name.value; + _this16._emitLine(blockName + ": " + blockName + ","); + }); + this._emitLine("root: root\n};"); + }; + _proto.compile = function compile2(node, frame) { + var _compile = this["compile" + node.typename]; + if (_compile) { + _compile.call(this, node, frame); + } else { + this.fail("compile: Cannot compile node: " + node.typename, node.lineno, node.colno); + } + }; + _proto.getCode = function getCode() { + return this.codebuf.join(""); + }; + return Compiler2; + }(Obj); + module2.exports = { + compile: function compile2(src, asyncFilters, extensions, name, opts) { + if (opts === void 0) { + opts = {}; + } + var c = new Compiler(name, opts.throwOnUndefined); + var preprocessors = (extensions || []).map(function(ext) { + return ext.preprocess; + }).filter(function(f) { + return !!f; + }); + var processedSrc = preprocessors.reduce(function(s, processor) { + return processor(s); + }, src); + c.compile(transformer.transform(parser2.parse(processedSrc, extensions, opts), asyncFilters, name)); + return c.getCode(); + }, + Compiler + }; +}); + +// ../../node_modules/nunjucks/src/filters.js +var require_filters = __commonJS((exports2, module2) => { + "use strict"; + var lib2 = require_lib(); + var r = require_runtime(); + var _exports = module2.exports = {}; + function normalize(value, defaultValue) { + if (value === null || value === void 0 || value === false) { + return defaultValue; + } + return value; + } + _exports.abs = Math.abs; + function isNaN2(num) { + return num !== num; + } + function batch(arr, linecount, fillWith) { + var i; + var res = []; + var tmp = []; + for (i = 0; i < arr.length; i++) { + if (i % linecount === 0 && tmp.length) { + res.push(tmp); + tmp = []; + } + tmp.push(arr[i]); + } + if (tmp.length) { + if (fillWith) { + for (i = tmp.length; i < linecount; i++) { + tmp.push(fillWith); + } + } + res.push(tmp); + } + return res; + } + _exports.batch = batch; + function capitalize(str) { + str = normalize(str, ""); + var ret = str.toLowerCase(); + return r.copySafeness(str, ret.charAt(0).toUpperCase() + ret.slice(1)); + } + _exports.capitalize = capitalize; + function center(str, width) { + str = normalize(str, ""); + width = width || 80; + if (str.length >= width) { + return str; + } + var spaces = width - str.length; + var pre = lib2.repeat(" ", spaces / 2 - spaces % 2); + var post = lib2.repeat(" ", spaces / 2); + return r.copySafeness(str, pre + str + post); + } + _exports.center = center; + function default_(val, def, bool) { + if (bool) { + return val || def; + } else { + return val !== void 0 ? val : def; + } + } + _exports["default"] = default_; + function dictsort(val, caseSensitive, by) { + if (!lib2.isObject(val)) { + throw new lib2.TemplateError("dictsort filter: val must be an object"); + } + var array = []; + for (var k in val) { + array.push([k, val[k]]); + } + var si; + if (by === void 0 || by === "key") { + si = 0; + } else if (by === "value") { + si = 1; + } else { + throw new lib2.TemplateError("dictsort filter: You can only sort by either key or value"); + } + array.sort(function(t1, t2) { + var a = t1[si]; + var b = t2[si]; + if (!caseSensitive) { + if (lib2.isString(a)) { + a = a.toUpperCase(); + } + if (lib2.isString(b)) { + b = b.toUpperCase(); + } + } + return a > b ? 1 : a === b ? 0 : -1; + }); + return array; + } + _exports.dictsort = dictsort; + function dump(obj, spaces) { + return JSON.stringify(obj, null, spaces); + } + _exports.dump = dump; + function escape(str) { + if (str instanceof r.SafeString) { + return str; + } + str = str === null || str === void 0 ? "" : str; + return r.markSafe(lib2.escape(str.toString())); + } + _exports.escape = escape; + function safe(str) { + if (str instanceof r.SafeString) { + return str; + } + str = str === null || str === void 0 ? "" : str; + return r.markSafe(str.toString()); + } + _exports.safe = safe; + function first(arr) { + return arr[0]; + } + _exports.first = first; + function forceescape(str) { + str = str === null || str === void 0 ? "" : str; + return r.markSafe(lib2.escape(str.toString())); + } + _exports.forceescape = forceescape; + function groupby(arr, attr) { + return lib2.groupBy(arr, attr, this.env.opts.throwOnUndefined); + } + _exports.groupby = groupby; + function indent(str, width, indentfirst) { + str = normalize(str, ""); + if (str === "") { + return ""; + } + width = width || 4; + var lines = str.split("\n"); + var sp = lib2.repeat(" ", width); + var res = lines.map(function(l, i) { + return i === 0 && !indentfirst ? l : "" + sp + l; + }).join("\n"); + return r.copySafeness(str, res); + } + _exports.indent = indent; + function join(arr, del, attr) { + del = del || ""; + if (attr) { + arr = lib2.map(arr, function(v) { + return v[attr]; + }); + } + return arr.join(del); + } + _exports.join = join; + function last(arr) { + return arr[arr.length - 1]; + } + _exports.last = last; + function lengthFilter(val) { + var value = normalize(val, ""); + if (value !== void 0) { + if (typeof Map === "function" && value instanceof Map || typeof Set === "function" && value instanceof Set) { + return value.size; + } + if (lib2.isObject(value) && !(value instanceof r.SafeString)) { + return lib2.keys(value).length; + } + return value.length; + } + return 0; + } + _exports.length = lengthFilter; + function list(val) { + if (lib2.isString(val)) { + return val.split(""); + } else if (lib2.isObject(val)) { + return lib2._entries(val || {}).map(function(_ref) { + var key = _ref[0], value = _ref[1]; + return { + key, + value + }; + }); + } else if (lib2.isArray(val)) { + return val; + } else { + throw new lib2.TemplateError("list filter: type not iterable"); + } + } + _exports.list = list; + function lower(str) { + str = normalize(str, ""); + return str.toLowerCase(); + } + _exports.lower = lower; + function nl2br(str) { + if (str === null || str === void 0) { + return ""; + } + return r.copySafeness(str, str.replace(/\r\n|\n/g, "
\n")); + } + _exports.nl2br = nl2br; + function random(arr) { + return arr[Math.floor(Math.random() * arr.length)]; + } + _exports.random = random; + function getSelectOrReject(expectedTestResult) { + function filter(arr, testName, secondArg) { + if (testName === void 0) { + testName = "truthy"; + } + var context = this; + var test = context.env.getTest(testName); + return lib2.toArray(arr).filter(function examineTestResult(item) { + return test.call(context, item, secondArg) === expectedTestResult; + }); + } + return filter; + } + _exports.reject = getSelectOrReject(false); + function rejectattr(arr, attr) { + return arr.filter(function(item) { + return !item[attr]; + }); + } + _exports.rejectattr = rejectattr; + _exports.select = getSelectOrReject(true); + function selectattr(arr, attr) { + return arr.filter(function(item) { + return !!item[attr]; + }); + } + _exports.selectattr = selectattr; + function replace(str, old, new_, maxCount) { + var originalStr = str; + if (old instanceof RegExp) { + return str.replace(old, new_); + } + if (typeof maxCount === "undefined") { + maxCount = -1; + } + var res = ""; + if (typeof old === "number") { + old = "" + old; + } else if (typeof old !== "string") { + return str; + } + if (typeof str === "number") { + str = "" + str; + } + if (typeof str !== "string" && !(str instanceof r.SafeString)) { + return str; + } + if (old === "") { + res = new_ + str.split("").join(new_) + new_; + return r.copySafeness(str, res); + } + var nextIndex = str.indexOf(old); + if (maxCount === 0 || nextIndex === -1) { + return str; + } + var pos = 0; + var count = 0; + while (nextIndex > -1 && (maxCount === -1 || count < maxCount)) { + res += str.substring(pos, nextIndex) + new_; + pos = nextIndex + old.length; + count++; + nextIndex = str.indexOf(old, pos); + } + if (pos < str.length) { + res += str.substring(pos); + } + return r.copySafeness(originalStr, res); + } + _exports.replace = replace; + function reverse(val) { + var arr; + if (lib2.isString(val)) { + arr = list(val); + } else { + arr = lib2.map(val, function(v) { + return v; + }); + } + arr.reverse(); + if (lib2.isString(val)) { + return r.copySafeness(val, arr.join("")); + } + return arr; + } + _exports.reverse = reverse; + function round(val, precision, method) { + precision = precision || 0; + var factor = Math.pow(10, precision); + var rounder; + if (method === "ceil") { + rounder = Math.ceil; + } else if (method === "floor") { + rounder = Math.floor; + } else { + rounder = Math.round; + } + return rounder(val * factor) / factor; + } + _exports.round = round; + function slice(arr, slices, fillWith) { + var sliceLength = Math.floor(arr.length / slices); + var extra = arr.length % slices; + var res = []; + var offset = 0; + for (var i = 0; i < slices; i++) { + var start = offset + i * sliceLength; + if (i < extra) { + offset++; + } + var end = offset + (i + 1) * sliceLength; + var currSlice = arr.slice(start, end); + if (fillWith && i >= extra) { + currSlice.push(fillWith); + } + res.push(currSlice); + } + return res; + } + _exports.slice = slice; + function sum(arr, attr, start) { + if (start === void 0) { + start = 0; + } + if (attr) { + arr = lib2.map(arr, function(v) { + return v[attr]; + }); + } + return start + arr.reduce(function(a, b) { + return a + b; + }, 0); + } + _exports.sum = sum; + _exports.sort = r.makeMacro(["value", "reverse", "case_sensitive", "attribute"], [], function sortFilter(arr, reversed, caseSens, attr) { + var _this = this; + var array = lib2.map(arr, function(v) { + return v; + }); + var getAttribute = lib2.getAttrGetter(attr); + array.sort(function(a, b) { + var x = attr ? getAttribute(a) : a; + var y = attr ? getAttribute(b) : b; + if (_this.env.opts.throwOnUndefined && attr && (x === void 0 || y === void 0)) { + throw new TypeError('sort: attribute "' + attr + '" resolved to undefined'); + } + if (!caseSens && lib2.isString(x) && lib2.isString(y)) { + x = x.toLowerCase(); + y = y.toLowerCase(); + } + if (x < y) { + return reversed ? 1 : -1; + } else if (x > y) { + return reversed ? -1 : 1; + } else { + return 0; + } + }); + return array; + }); + function string(obj) { + return r.copySafeness(obj, obj); + } + _exports.string = string; + function striptags(input, preserveLinebreaks) { + input = normalize(input, ""); + var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi; + var trimmedInput = trim(input.replace(tags, "")); + var res = ""; + if (preserveLinebreaks) { + res = trimmedInput.replace(/^ +| +$/gm, "").replace(/ +/g, " ").replace(/(\r\n)/g, "\n").replace(/\n\n\n+/g, "\n\n"); + } else { + res = trimmedInput.replace(/\s+/gi, " "); + } + return r.copySafeness(input, res); + } + _exports.striptags = striptags; + function title(str) { + str = normalize(str, ""); + var words = str.split(" ").map(function(word) { + return capitalize(word); + }); + return r.copySafeness(str, words.join(" ")); + } + _exports.title = title; + function trim(str) { + return r.copySafeness(str, str.replace(/^\s*|\s*$/g, "")); + } + _exports.trim = trim; + function truncate(input, length, killwords, end) { + var orig = input; + input = normalize(input, ""); + length = length || 255; + if (input.length <= length) { + return input; + } + if (killwords) { + input = input.substring(0, length); + } else { + var idx = input.lastIndexOf(" ", length); + if (idx === -1) { + idx = length; + } + input = input.substring(0, idx); + } + input += end !== void 0 && end !== null ? end : "..."; + return r.copySafeness(orig, input); + } + _exports.truncate = truncate; + function upper(str) { + str = normalize(str, ""); + return str.toUpperCase(); + } + _exports.upper = upper; + function urlencode(obj) { + var enc = encodeURIComponent; + if (lib2.isString(obj)) { + return enc(obj); + } else { + var keyvals = lib2.isArray(obj) ? obj : lib2._entries(obj); + return keyvals.map(function(_ref2) { + var k = _ref2[0], v = _ref2[1]; + return enc(k) + "=" + enc(v); + }).join("&"); + } + } + _exports.urlencode = urlencode; + var puncRe = /^(?:\(|<|<)?(.*?)(?:\.|,|\)|\n|>)?$/; + var emailRe = /^[\w.!#$%&'*+\-\/=?\^`{|}~]+@[a-z\d\-]+(\.[a-z\d\-]+)+$/i; + var httpHttpsRe = /^https?:\/\/.*$/; + var wwwRe = /^www\./; + var tldRe = /\.(?:org|net|com)(?:\:|\/|$)/; + function urlize(str, length, nofollow) { + if (isNaN2(length)) { + length = Infinity; + } + var noFollowAttr = nofollow === true ? ' rel="nofollow"' : ""; + var words = str.split(/(\s+)/).filter(function(word) { + return word && word.length; + }).map(function(word) { + var matches = word.match(puncRe); + var possibleUrl = matches ? matches[1] : word; + var shortUrl = possibleUrl.substr(0, length); + if (httpHttpsRe.test(possibleUrl)) { + return '" + shortUrl + ""; + } + if (wwwRe.test(possibleUrl)) { + return '" + shortUrl + ""; + } + if (emailRe.test(possibleUrl)) { + return '' + possibleUrl + ""; + } + if (tldRe.test(possibleUrl)) { + return '" + shortUrl + ""; + } + return word; + }); + return words.join(""); + } + _exports.urlize = urlize; + function wordcount(str) { + str = normalize(str, ""); + var words = str ? str.match(/\w+/g) : null; + return words ? words.length : null; + } + _exports.wordcount = wordcount; + function float(val, def) { + var res = parseFloat(val); + return isNaN2(res) ? def : res; + } + _exports.float = float; + var intFilter = r.makeMacro(["value", "default", "base"], [], function doInt(value, defaultValue, base) { + if (base === void 0) { + base = 10; + } + var res = parseInt(value, base); + return isNaN2(res) ? defaultValue : res; + }); + _exports.int = intFilter; + _exports.d = _exports.default; + _exports.e = _exports.escape; +}); + +// ../../node_modules/nunjucks/src/loader.js +var require_loader = __commonJS((exports2, module2) => { + "use strict"; + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var path = require("path"); + var _require2 = require_object(); + var EmitterObj = _require2.EmitterObj; + module2.exports = /* @__PURE__ */ function(_EmitterObj) { + _inheritsLoose(Loader2, _EmitterObj); + function Loader2() { + return _EmitterObj.apply(this, arguments) || this; + } + var _proto = Loader2.prototype; + _proto.resolve = function resolve(from, to) { + return path.resolve(path.dirname(from), to); + }; + _proto.isRelative = function isRelative(filename) { + return filename.indexOf("./") === 0 || filename.indexOf("../") === 0; + }; + return Loader2; + }(EmitterObj); +}); + +// ../../node_modules/nunjucks/src/precompiled-loader.js +var require_precompiled_loader = __commonJS((exports2, module2) => { + "use strict"; + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var Loader2 = require_loader(); + var PrecompiledLoader = /* @__PURE__ */ function(_Loader) { + _inheritsLoose(PrecompiledLoader2, _Loader); + function PrecompiledLoader2(compiledTemplates) { + var _this; + _this = _Loader.call(this) || this; + _this.precompiled = compiledTemplates || {}; + return _this; + } + var _proto = PrecompiledLoader2.prototype; + _proto.getSource = function getSource(name) { + if (this.precompiled[name]) { + return { + src: { + type: "code", + obj: this.precompiled[name] + }, + path: name + }; + } + return null; + }; + return PrecompiledLoader2; + }(Loader2); + module2.exports = { + PrecompiledLoader + }; +}); + +// ../../node_modules/picomatch/lib/constants.js +var require_constants = __commonJS((exports2, module2) => { + "use strict"; + var path = require("path"); + var WIN_SLASH = "\\\\/"; + var WIN_NO_SLASH = `[^${WIN_SLASH}]`; + var DOT_LITERAL = "\\."; + var PLUS_LITERAL = "\\+"; + var QMARK_LITERAL = "\\?"; + var SLASH_LITERAL = "\\/"; + var ONE_CHAR = "(?=.)"; + var QMARK = "[^/]"; + var END_ANCHOR = `(?:${SLASH_LITERAL}|$)`; + var START_ANCHOR = `(?:^|${SLASH_LITERAL})`; + var DOTS_SLASH = `${DOT_LITERAL}{1,2}${END_ANCHOR}`; + var NO_DOT = `(?!${DOT_LITERAL})`; + var NO_DOTS = `(?!${START_ANCHOR}${DOTS_SLASH})`; + var NO_DOT_SLASH = `(?!${DOT_LITERAL}{0,1}${END_ANCHOR})`; + var NO_DOTS_SLASH = `(?!${DOTS_SLASH})`; + var QMARK_NO_DOT = `[^.${SLASH_LITERAL}]`; + var STAR = `${QMARK}*?`; + var POSIX_CHARS = { + DOT_LITERAL, + PLUS_LITERAL, + QMARK_LITERAL, + SLASH_LITERAL, + ONE_CHAR, + QMARK, + END_ANCHOR, + DOTS_SLASH, + NO_DOT, + NO_DOTS, + NO_DOT_SLASH, + NO_DOTS_SLASH, + QMARK_NO_DOT, + STAR, + START_ANCHOR + }; + var WINDOWS_CHARS = { + ...POSIX_CHARS, + SLASH_LITERAL: `[${WIN_SLASH}]`, + QMARK: WIN_NO_SLASH, + STAR: `${WIN_NO_SLASH}*?`, + DOTS_SLASH: `${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$)`, + NO_DOT: `(?!${DOT_LITERAL})`, + NO_DOTS: `(?!(?:^|[${WIN_SLASH}])${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`, + NO_DOT_SLASH: `(?!${DOT_LITERAL}{0,1}(?:[${WIN_SLASH}]|$))`, + NO_DOTS_SLASH: `(?!${DOT_LITERAL}{1,2}(?:[${WIN_SLASH}]|$))`, + QMARK_NO_DOT: `[^.${WIN_SLASH}]`, + START_ANCHOR: `(?:^|[${WIN_SLASH}])`, + END_ANCHOR: `(?:[${WIN_SLASH}]|$)` + }; + var POSIX_REGEX_SOURCE = { + alnum: "a-zA-Z0-9", + alpha: "a-zA-Z", + ascii: "\\x00-\\x7F", + blank: " \\t", + cntrl: "\\x00-\\x1F\\x7F", + digit: "0-9", + graph: "\\x21-\\x7E", + lower: "a-z", + print: "\\x20-\\x7E ", + punct: "\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~", + space: " \\t\\r\\n\\v\\f", + upper: "A-Z", + word: "A-Za-z0-9_", + xdigit: "A-Fa-f0-9" + }; + module2.exports = { + MAX_LENGTH: 1024 * 64, + POSIX_REGEX_SOURCE, + REGEX_BACKSLASH: /\\(?![*+?^${}(|)[\]])/g, + REGEX_NON_SPECIAL_CHARS: /^[^@![\].,$*+?^{}()|\\/]+/, + REGEX_SPECIAL_CHARS: /[-*+?.^${}(|)[\]]/, + REGEX_SPECIAL_CHARS_BACKREF: /(\\?)((\W)(\3*))/g, + REGEX_SPECIAL_CHARS_GLOBAL: /([-*+?.^${}(|)[\]])/g, + REGEX_REMOVE_BACKSLASH: /(?:\[.*?[^\\]\]|\\(?=.))/g, + REPLACEMENTS: { + "***": "*", + "**/**": "**", + "**/**/**": "**" + }, + CHAR_0: 48, + CHAR_9: 57, + CHAR_UPPERCASE_A: 65, + CHAR_LOWERCASE_A: 97, + CHAR_UPPERCASE_Z: 90, + CHAR_LOWERCASE_Z: 122, + CHAR_LEFT_PARENTHESES: 40, + CHAR_RIGHT_PARENTHESES: 41, + CHAR_ASTERISK: 42, + CHAR_AMPERSAND: 38, + CHAR_AT: 64, + CHAR_BACKWARD_SLASH: 92, + CHAR_CARRIAGE_RETURN: 13, + CHAR_CIRCUMFLEX_ACCENT: 94, + CHAR_COLON: 58, + CHAR_COMMA: 44, + CHAR_DOT: 46, + CHAR_DOUBLE_QUOTE: 34, + CHAR_EQUAL: 61, + CHAR_EXCLAMATION_MARK: 33, + CHAR_FORM_FEED: 12, + CHAR_FORWARD_SLASH: 47, + CHAR_GRAVE_ACCENT: 96, + CHAR_HASH: 35, + CHAR_HYPHEN_MINUS: 45, + CHAR_LEFT_ANGLE_BRACKET: 60, + CHAR_LEFT_CURLY_BRACE: 123, + CHAR_LEFT_SQUARE_BRACKET: 91, + CHAR_LINE_FEED: 10, + CHAR_NO_BREAK_SPACE: 160, + CHAR_PERCENT: 37, + CHAR_PLUS: 43, + CHAR_QUESTION_MARK: 63, + CHAR_RIGHT_ANGLE_BRACKET: 62, + CHAR_RIGHT_CURLY_BRACE: 125, + CHAR_RIGHT_SQUARE_BRACKET: 93, + CHAR_SEMICOLON: 59, + CHAR_SINGLE_QUOTE: 39, + CHAR_SPACE: 32, + CHAR_TAB: 9, + CHAR_UNDERSCORE: 95, + CHAR_VERTICAL_LINE: 124, + CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279, + SEP: path.sep, + extglobChars(chars) { + return { + "!": {type: "negate", open: "(?:(?!(?:", close: `))${chars.STAR})`}, + "?": {type: "qmark", open: "(?:", close: ")?"}, + "+": {type: "plus", open: "(?:", close: ")+"}, + "*": {type: "star", open: "(?:", close: ")*"}, + "@": {type: "at", open: "(?:", close: ")"} + }; + }, + globChars(win32) { + return win32 === true ? WINDOWS_CHARS : POSIX_CHARS; + } + }; +}); + +// ../../node_modules/picomatch/lib/utils.js +var require_utils = __commonJS((exports2) => { + "use strict"; + var path = require("path"); + var win32 = process.platform === "win32"; + var { + REGEX_BACKSLASH, + REGEX_REMOVE_BACKSLASH, + REGEX_SPECIAL_CHARS, + REGEX_SPECIAL_CHARS_GLOBAL + } = require_constants(); + exports2.isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val); + exports2.hasRegexChars = (str) => REGEX_SPECIAL_CHARS.test(str); + exports2.isRegexChar = (str) => str.length === 1 && exports2.hasRegexChars(str); + exports2.escapeRegex = (str) => str.replace(REGEX_SPECIAL_CHARS_GLOBAL, "\\$1"); + exports2.toPosixSlashes = (str) => str.replace(REGEX_BACKSLASH, "/"); + exports2.removeBackslashes = (str) => { + return str.replace(REGEX_REMOVE_BACKSLASH, (match) => { + return match === "\\" ? "" : match; + }); + }; + exports2.supportsLookbehinds = () => { + const segs = process.version.slice(1).split(".").map(Number); + if (segs.length === 3 && segs[0] >= 9 || segs[0] === 8 && segs[1] >= 10) { + return true; + } + return false; + }; + exports2.isWindows = (options) => { + if (options && typeof options.windows === "boolean") { + return options.windows; + } + return win32 === true || path.sep === "\\"; + }; + exports2.escapeLast = (input, char, lastIdx) => { + const idx = input.lastIndexOf(char, lastIdx); + if (idx === -1) + return input; + if (input[idx - 1] === "\\") + return exports2.escapeLast(input, char, idx - 1); + return `${input.slice(0, idx)}\\${input.slice(idx)}`; + }; + exports2.removePrefix = (input, state = {}) => { + let output = input; + if (output.startsWith("./")) { + output = output.slice(2); + state.prefix = "./"; + } + return output; + }; + exports2.wrapOutput = (input, state = {}, options = {}) => { + const prepend = options.contains ? "" : "^"; + const append = options.contains ? "" : "$"; + let output = `${prepend}(?:${input})${append}`; + if (state.negated === true) { + output = `(?:^(?!${output}).*$)`; + } + return output; + }; +}); + +// ../../node_modules/picomatch/lib/scan.js +var require_scan = __commonJS((exports2, module2) => { + "use strict"; + var utils = require_utils(); + var { + CHAR_ASTERISK, + CHAR_AT, + CHAR_BACKWARD_SLASH, + CHAR_COMMA, + CHAR_DOT, + CHAR_EXCLAMATION_MARK, + CHAR_FORWARD_SLASH, + CHAR_LEFT_CURLY_BRACE, + CHAR_LEFT_PARENTHESES, + CHAR_LEFT_SQUARE_BRACKET, + CHAR_PLUS, + CHAR_QUESTION_MARK, + CHAR_RIGHT_CURLY_BRACE, + CHAR_RIGHT_PARENTHESES, + CHAR_RIGHT_SQUARE_BRACKET + } = require_constants(); + var isPathSeparator = (code) => { + return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; + }; + var depth = (token) => { + if (token.isPrefix !== true) { + token.depth = token.isGlobstar ? Infinity : 1; + } + }; + var scan = (input, options) => { + const opts = options || {}; + const length = input.length - 1; + const scanToEnd = opts.parts === true || opts.scanToEnd === true; + const slashes = []; + const tokens = []; + const parts = []; + let str = input; + let index = -1; + let start = 0; + let lastIndex = 0; + let isBrace = false; + let isBracket = false; + let isGlob = false; + let isExtglob = false; + let isGlobstar = false; + let braceEscaped = false; + let backslashes = false; + let negated = false; + let negatedExtglob = false; + let finished = false; + let braces = 0; + let prev; + let code; + let token = {value: "", depth: 0, isGlob: false}; + const eos = () => index >= length; + const peek = () => str.charCodeAt(index + 1); + const advance = () => { + prev = code; + return str.charCodeAt(++index); + }; + while (index < length) { + code = advance(); + let next; + if (code === CHAR_BACKWARD_SLASH) { + backslashes = token.backslashes = true; + code = advance(); + if (code === CHAR_LEFT_CURLY_BRACE) { + braceEscaped = true; + } + continue; + } + if (braceEscaped === true || code === CHAR_LEFT_CURLY_BRACE) { + braces++; + while (eos() !== true && (code = advance())) { + if (code === CHAR_BACKWARD_SLASH) { + backslashes = token.backslashes = true; + advance(); + continue; + } + if (code === CHAR_LEFT_CURLY_BRACE) { + braces++; + continue; + } + if (braceEscaped !== true && code === CHAR_DOT && (code = advance()) === CHAR_DOT) { + isBrace = token.isBrace = true; + isGlob = token.isGlob = true; + finished = true; + if (scanToEnd === true) { + continue; + } + break; + } + if (braceEscaped !== true && code === CHAR_COMMA) { + isBrace = token.isBrace = true; + isGlob = token.isGlob = true; + finished = true; + if (scanToEnd === true) { + continue; + } + break; + } + if (code === CHAR_RIGHT_CURLY_BRACE) { + braces--; + if (braces === 0) { + braceEscaped = false; + isBrace = token.isBrace = true; + finished = true; + break; + } + } + } + if (scanToEnd === true) { + continue; + } + break; + } + if (code === CHAR_FORWARD_SLASH) { + slashes.push(index); + tokens.push(token); + token = {value: "", depth: 0, isGlob: false}; + if (finished === true) + continue; + if (prev === CHAR_DOT && index === start + 1) { + start += 2; + continue; + } + lastIndex = index + 1; + continue; + } + if (opts.noext !== true) { + const isExtglobChar = code === CHAR_PLUS || code === CHAR_AT || code === CHAR_ASTERISK || code === CHAR_QUESTION_MARK || code === CHAR_EXCLAMATION_MARK; + if (isExtglobChar === true && peek() === CHAR_LEFT_PARENTHESES) { + isGlob = token.isGlob = true; + isExtglob = token.isExtglob = true; + finished = true; + if (code === CHAR_EXCLAMATION_MARK && index === start) { + negatedExtglob = true; + } + if (scanToEnd === true) { + while (eos() !== true && (code = advance())) { + if (code === CHAR_BACKWARD_SLASH) { + backslashes = token.backslashes = true; + code = advance(); + continue; + } + if (code === CHAR_RIGHT_PARENTHESES) { + isGlob = token.isGlob = true; + finished = true; + break; + } + } + continue; + } + break; + } + } + if (code === CHAR_ASTERISK) { + if (prev === CHAR_ASTERISK) + isGlobstar = token.isGlobstar = true; + isGlob = token.isGlob = true; + finished = true; + if (scanToEnd === true) { + continue; + } + break; + } + if (code === CHAR_QUESTION_MARK) { + isGlob = token.isGlob = true; + finished = true; + if (scanToEnd === true) { + continue; + } + break; + } + if (code === CHAR_LEFT_SQUARE_BRACKET) { + while (eos() !== true && (next = advance())) { + if (next === CHAR_BACKWARD_SLASH) { + backslashes = token.backslashes = true; + advance(); + continue; + } + if (next === CHAR_RIGHT_SQUARE_BRACKET) { + isBracket = token.isBracket = true; + isGlob = token.isGlob = true; + finished = true; + break; + } + } + if (scanToEnd === true) { + continue; + } + break; + } + if (opts.nonegate !== true && code === CHAR_EXCLAMATION_MARK && index === start) { + negated = token.negated = true; + start++; + continue; + } + if (opts.noparen !== true && code === CHAR_LEFT_PARENTHESES) { + isGlob = token.isGlob = true; + if (scanToEnd === true) { + while (eos() !== true && (code = advance())) { + if (code === CHAR_LEFT_PARENTHESES) { + backslashes = token.backslashes = true; + code = advance(); + continue; + } + if (code === CHAR_RIGHT_PARENTHESES) { + finished = true; + break; + } + } + continue; + } + break; + } + if (isGlob === true) { + finished = true; + if (scanToEnd === true) { + continue; + } + break; + } + } + if (opts.noext === true) { + isExtglob = false; + isGlob = false; + } + let base = str; + let prefix = ""; + let glob = ""; + if (start > 0) { + prefix = str.slice(0, start); + str = str.slice(start); + lastIndex -= start; + } + if (base && isGlob === true && lastIndex > 0) { + base = str.slice(0, lastIndex); + glob = str.slice(lastIndex); + } else if (isGlob === true) { + base = ""; + glob = str; + } else { + base = str; + } + if (base && base !== "" && base !== "/" && base !== str) { + if (isPathSeparator(base.charCodeAt(base.length - 1))) { + base = base.slice(0, -1); + } + } + if (opts.unescape === true) { + if (glob) + glob = utils.removeBackslashes(glob); + if (base && backslashes === true) { + base = utils.removeBackslashes(base); + } + } + const state = { + prefix, + input, + start, + base, + glob, + isBrace, + isBracket, + isGlob, + isExtglob, + isGlobstar, + negated, + negatedExtglob + }; + if (opts.tokens === true) { + state.maxDepth = 0; + if (!isPathSeparator(code)) { + tokens.push(token); + } + state.tokens = tokens; + } + if (opts.parts === true || opts.tokens === true) { + let prevIndex; + for (let idx = 0; idx < slashes.length; idx++) { + const n = prevIndex ? prevIndex + 1 : start; + const i = slashes[idx]; + const value = input.slice(n, i); + if (opts.tokens) { + if (idx === 0 && start !== 0) { + tokens[idx].isPrefix = true; + tokens[idx].value = prefix; + } else { + tokens[idx].value = value; + } + depth(tokens[idx]); + state.maxDepth += tokens[idx].depth; + } + if (idx !== 0 || value !== "") { + parts.push(value); + } + prevIndex = i; + } + if (prevIndex && prevIndex + 1 < input.length) { + const value = input.slice(prevIndex + 1); + parts.push(value); + if (opts.tokens) { + tokens[tokens.length - 1].value = value; + depth(tokens[tokens.length - 1]); + state.maxDepth += tokens[tokens.length - 1].depth; + } + } + state.slashes = slashes; + state.parts = parts; + } + return state; + }; + module2.exports = scan; +}); + +// ../../node_modules/picomatch/lib/parse.js +var require_parse = __commonJS((exports2, module2) => { + "use strict"; + var constants = require_constants(); + var utils = require_utils(); + var { + MAX_LENGTH, + POSIX_REGEX_SOURCE, + REGEX_NON_SPECIAL_CHARS, + REGEX_SPECIAL_CHARS_BACKREF, + REPLACEMENTS + } = constants; + var expandRange = (args, options) => { + if (typeof options.expandRange === "function") { + return options.expandRange(...args, options); + } + args.sort(); + const value = `[${args.join("-")}]`; + try { + new RegExp(value); + } catch (ex) { + return args.map((v) => utils.escapeRegex(v)).join(".."); + } + return value; + }; + var syntaxError = (type, char) => { + return `Missing ${type}: "${char}" - use "\\\\${char}" to match literal characters`; + }; + var parse = (input, options) => { + if (typeof input !== "string") { + throw new TypeError("Expected a string"); + } + input = REPLACEMENTS[input] || input; + const opts = {...options}; + const max = typeof opts.maxLength === "number" ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; + let len = input.length; + if (len > max) { + throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); + } + const bos = {type: "bos", value: "", output: opts.prepend || ""}; + const tokens = [bos]; + const capture = opts.capture ? "" : "?:"; + const win32 = utils.isWindows(options); + const PLATFORM_CHARS = constants.globChars(win32); + const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_CHARS); + const { + DOT_LITERAL, + PLUS_LITERAL, + SLASH_LITERAL, + ONE_CHAR, + DOTS_SLASH, + NO_DOT, + NO_DOT_SLASH, + NO_DOTS_SLASH, + QMARK, + QMARK_NO_DOT, + STAR, + START_ANCHOR + } = PLATFORM_CHARS; + const globstar = (opts2) => { + return `(${capture}(?:(?!${START_ANCHOR}${opts2.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; + }; + const nodot = opts.dot ? "" : NO_DOT; + const qmarkNoDot = opts.dot ? QMARK : QMARK_NO_DOT; + let star = opts.bash === true ? globstar(opts) : STAR; + if (opts.capture) { + star = `(${star})`; + } + if (typeof opts.noext === "boolean") { + opts.noextglob = opts.noext; + } + const state = { + input, + index: -1, + start: 0, + dot: opts.dot === true, + consumed: "", + output: "", + prefix: "", + backtrack: false, + negated: false, + brackets: 0, + braces: 0, + parens: 0, + quotes: 0, + globstar: false, + tokens + }; + input = utils.removePrefix(input, state); + len = input.length; + const extglobs = []; + const braces = []; + const stack = []; + let prev = bos; + let value; + const eos = () => state.index === len - 1; + const peek = state.peek = (n = 1) => input[state.index + n]; + const advance = state.advance = () => input[++state.index] || ""; + const remaining = () => input.slice(state.index + 1); + const consume = (value2 = "", num = 0) => { + state.consumed += value2; + state.index += num; + }; + const append = (token) => { + state.output += token.output != null ? token.output : token.value; + consume(token.value); + }; + const negate = () => { + let count = 1; + while (peek() === "!" && (peek(2) !== "(" || peek(3) === "?")) { + advance(); + state.start++; + count++; + } + if (count % 2 === 0) { + return false; + } + state.negated = true; + state.start++; + return true; + }; + const increment = (type) => { + state[type]++; + stack.push(type); + }; + const decrement = (type) => { + state[type]--; + stack.pop(); + }; + const push = (tok) => { + if (prev.type === "globstar") { + const isBrace = state.braces > 0 && (tok.type === "comma" || tok.type === "brace"); + const isExtglob = tok.extglob === true || extglobs.length && (tok.type === "pipe" || tok.type === "paren"); + if (tok.type !== "slash" && tok.type !== "paren" && !isBrace && !isExtglob) { + state.output = state.output.slice(0, -prev.output.length); + prev.type = "star"; + prev.value = "*"; + prev.output = star; + state.output += prev.output; + } + } + if (extglobs.length && tok.type !== "paren") { + extglobs[extglobs.length - 1].inner += tok.value; + } + if (tok.value || tok.output) + append(tok); + if (prev && prev.type === "text" && tok.type === "text") { + prev.value += tok.value; + prev.output = (prev.output || "") + tok.value; + return; + } + tok.prev = prev; + tokens.push(tok); + prev = tok; + }; + const extglobOpen = (type, value2) => { + const token = {...EXTGLOB_CHARS[value2], conditions: 1, inner: ""}; + token.prev = prev; + token.parens = state.parens; + token.output = state.output; + const output = (opts.capture ? "(" : "") + token.open; + increment("parens"); + push({type, value: value2, output: state.output ? "" : ONE_CHAR}); + push({type: "paren", extglob: true, value: advance(), output}); + extglobs.push(token); + }; + const extglobClose = (token) => { + let output = token.close + (opts.capture ? ")" : ""); + let rest; + if (token.type === "negate") { + let extglobStar = star; + if (token.inner && token.inner.length > 1 && token.inner.includes("/")) { + extglobStar = globstar(opts); + } + if (extglobStar !== star || eos() || /^\)+$/.test(remaining())) { + output = token.close = `)$))${extglobStar}`; + } + if (token.inner.includes("*") && (rest = remaining()) && /^\.[^\\/.]+$/.test(rest)) { + output = token.close = `)${rest})${extglobStar})`; + } + if (token.prev.type === "bos") { + state.negatedExtglob = true; + } + } + push({type: "paren", extglob: true, value, output}); + decrement("parens"); + }; + if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) { + let backslashes = false; + let output = input.replace(REGEX_SPECIAL_CHARS_BACKREF, (m, esc, chars, first, rest, index) => { + if (first === "\\") { + backslashes = true; + return m; + } + if (first === "?") { + if (esc) { + return esc + first + (rest ? QMARK.repeat(rest.length) : ""); + } + if (index === 0) { + return qmarkNoDot + (rest ? QMARK.repeat(rest.length) : ""); + } + return QMARK.repeat(chars.length); + } + if (first === ".") { + return DOT_LITERAL.repeat(chars.length); + } + if (first === "*") { + if (esc) { + return esc + first + (rest ? star : ""); + } + return star; + } + return esc ? m : `\\${m}`; + }); + if (backslashes === true) { + if (opts.unescape === true) { + output = output.replace(/\\/g, ""); + } else { + output = output.replace(/\\+/g, (m) => { + return m.length % 2 === 0 ? "\\\\" : m ? "\\" : ""; + }); + } + } + if (output === input && opts.contains === true) { + state.output = input; + return state; + } + state.output = utils.wrapOutput(output, state, options); + return state; + } + while (!eos()) { + value = advance(); + if (value === "\0") { + continue; + } + if (value === "\\") { + const next = peek(); + if (next === "/" && opts.bash !== true) { + continue; + } + if (next === "." || next === ";") { + continue; + } + if (!next) { + value += "\\"; + push({type: "text", value}); + continue; + } + const match = /^\\+/.exec(remaining()); + let slashes = 0; + if (match && match[0].length > 2) { + slashes = match[0].length; + state.index += slashes; + if (slashes % 2 !== 0) { + value += "\\"; + } + } + if (opts.unescape === true) { + value = advance(); + } else { + value += advance(); + } + if (state.brackets === 0) { + push({type: "text", value}); + continue; + } + } + if (state.brackets > 0 && (value !== "]" || prev.value === "[" || prev.value === "[^")) { + if (opts.posix !== false && value === ":") { + const inner = prev.value.slice(1); + if (inner.includes("[")) { + prev.posix = true; + if (inner.includes(":")) { + const idx = prev.value.lastIndexOf("["); + const pre = prev.value.slice(0, idx); + const rest2 = prev.value.slice(idx + 2); + const posix = POSIX_REGEX_SOURCE[rest2]; + if (posix) { + prev.value = pre + posix; + state.backtrack = true; + advance(); + if (!bos.output && tokens.indexOf(prev) === 1) { + bos.output = ONE_CHAR; + } + continue; + } + } + } + } + if (value === "[" && peek() !== ":" || value === "-" && peek() === "]") { + value = `\\${value}`; + } + if (value === "]" && (prev.value === "[" || prev.value === "[^")) { + value = `\\${value}`; + } + if (opts.posix === true && value === "!" && prev.value === "[") { + value = "^"; + } + prev.value += value; + append({value}); + continue; + } + if (state.quotes === 1 && value !== '"') { + value = utils.escapeRegex(value); + prev.value += value; + append({value}); + continue; + } + if (value === '"') { + state.quotes = state.quotes === 1 ? 0 : 1; + if (opts.keepQuotes === true) { + push({type: "text", value}); + } + continue; + } + if (value === "(") { + increment("parens"); + push({type: "paren", value}); + continue; + } + if (value === ")") { + if (state.parens === 0 && opts.strictBrackets === true) { + throw new SyntaxError(syntaxError("opening", "(")); + } + const extglob = extglobs[extglobs.length - 1]; + if (extglob && state.parens === extglob.parens + 1) { + extglobClose(extglobs.pop()); + continue; + } + push({type: "paren", value, output: state.parens ? ")" : "\\)"}); + decrement("parens"); + continue; + } + if (value === "[") { + if (opts.nobracket === true || !remaining().includes("]")) { + if (opts.nobracket !== true && opts.strictBrackets === true) { + throw new SyntaxError(syntaxError("closing", "]")); + } + value = `\\${value}`; + } else { + increment("brackets"); + } + push({type: "bracket", value}); + continue; + } + if (value === "]") { + if (opts.nobracket === true || prev && prev.type === "bracket" && prev.value.length === 1) { + push({type: "text", value, output: `\\${value}`}); + continue; + } + if (state.brackets === 0) { + if (opts.strictBrackets === true) { + throw new SyntaxError(syntaxError("opening", "[")); + } + push({type: "text", value, output: `\\${value}`}); + continue; + } + decrement("brackets"); + const prevValue = prev.value.slice(1); + if (prev.posix !== true && prevValue[0] === "^" && !prevValue.includes("/")) { + value = `/${value}`; + } + prev.value += value; + append({value}); + if (opts.literalBrackets === false || utils.hasRegexChars(prevValue)) { + continue; + } + const escaped = utils.escapeRegex(prev.value); + state.output = state.output.slice(0, -prev.value.length); + if (opts.literalBrackets === true) { + state.output += escaped; + prev.value = escaped; + continue; + } + prev.value = `(${capture}${escaped}|${prev.value})`; + state.output += prev.value; + continue; + } + if (value === "{" && opts.nobrace !== true) { + increment("braces"); + const open = { + type: "brace", + value, + output: "(", + outputIndex: state.output.length, + tokensIndex: state.tokens.length + }; + braces.push(open); + push(open); + continue; + } + if (value === "}") { + const brace = braces[braces.length - 1]; + if (opts.nobrace === true || !brace) { + push({type: "text", value, output: value}); + continue; + } + let output = ")"; + if (brace.dots === true) { + const arr = tokens.slice(); + const range = []; + for (let i = arr.length - 1; i >= 0; i--) { + tokens.pop(); + if (arr[i].type === "brace") { + break; + } + if (arr[i].type !== "dots") { + range.unshift(arr[i].value); + } + } + output = expandRange(range, opts); + state.backtrack = true; + } + if (brace.comma !== true && brace.dots !== true) { + const out = state.output.slice(0, brace.outputIndex); + const toks = state.tokens.slice(brace.tokensIndex); + brace.value = brace.output = "\\{"; + value = output = "\\}"; + state.output = out; + for (const t of toks) { + state.output += t.output || t.value; + } + } + push({type: "brace", value, output}); + decrement("braces"); + braces.pop(); + continue; + } + if (value === "|") { + if (extglobs.length > 0) { + extglobs[extglobs.length - 1].conditions++; + } + push({type: "text", value}); + continue; + } + if (value === ",") { + let output = value; + const brace = braces[braces.length - 1]; + if (brace && stack[stack.length - 1] === "braces") { + brace.comma = true; + output = "|"; + } + push({type: "comma", value, output}); + continue; + } + if (value === "/") { + if (prev.type === "dot" && state.index === state.start + 1) { + state.start = state.index + 1; + state.consumed = ""; + state.output = ""; + tokens.pop(); + prev = bos; + continue; + } + push({type: "slash", value, output: SLASH_LITERAL}); + continue; + } + if (value === ".") { + if (state.braces > 0 && prev.type === "dot") { + if (prev.value === ".") + prev.output = DOT_LITERAL; + const brace = braces[braces.length - 1]; + prev.type = "dots"; + prev.output += value; + prev.value += value; + brace.dots = true; + continue; + } + if (state.braces + state.parens === 0 && prev.type !== "bos" && prev.type !== "slash") { + push({type: "text", value, output: DOT_LITERAL}); + continue; + } + push({type: "dot", value, output: DOT_LITERAL}); + continue; + } + if (value === "?") { + const isGroup = prev && prev.value === "("; + if (!isGroup && opts.noextglob !== true && peek() === "(" && peek(2) !== "?") { + extglobOpen("qmark", value); + continue; + } + if (prev && prev.type === "paren") { + const next = peek(); + let output = value; + if (next === "<" && !utils.supportsLookbehinds()) { + throw new Error("Node.js v10 or higher is required for regex lookbehinds"); + } + if (prev.value === "(" && !/[!=<:]/.test(next) || next === "<" && !/<([!=]|\w+>)/.test(remaining())) { + output = `\\${value}`; + } + push({type: "text", value, output}); + continue; + } + if (opts.dot !== true && (prev.type === "slash" || prev.type === "bos")) { + push({type: "qmark", value, output: QMARK_NO_DOT}); + continue; + } + push({type: "qmark", value, output: QMARK}); + continue; + } + if (value === "!") { + if (opts.noextglob !== true && peek() === "(") { + if (peek(2) !== "?" || !/[!=<:]/.test(peek(3))) { + extglobOpen("negate", value); + continue; + } + } + if (opts.nonegate !== true && state.index === 0) { + negate(); + continue; + } + } + if (value === "+") { + if (opts.noextglob !== true && peek() === "(" && peek(2) !== "?") { + extglobOpen("plus", value); + continue; + } + if (prev && prev.value === "(" || opts.regex === false) { + push({type: "plus", value, output: PLUS_LITERAL}); + continue; + } + if (prev && (prev.type === "bracket" || prev.type === "paren" || prev.type === "brace") || state.parens > 0) { + push({type: "plus", value}); + continue; + } + push({type: "plus", value: PLUS_LITERAL}); + continue; + } + if (value === "@") { + if (opts.noextglob !== true && peek() === "(" && peek(2) !== "?") { + push({type: "at", extglob: true, value, output: ""}); + continue; + } + push({type: "text", value}); + continue; + } + if (value !== "*") { + if (value === "$" || value === "^") { + value = `\\${value}`; + } + const match = REGEX_NON_SPECIAL_CHARS.exec(remaining()); + if (match) { + value += match[0]; + state.index += match[0].length; + } + push({type: "text", value}); + continue; + } + if (prev && (prev.type === "globstar" || prev.star === true)) { + prev.type = "star"; + prev.star = true; + prev.value += value; + prev.output = star; + state.backtrack = true; + state.globstar = true; + consume(value); + continue; + } + let rest = remaining(); + if (opts.noextglob !== true && /^\([^?]/.test(rest)) { + extglobOpen("star", value); + continue; + } + if (prev.type === "star") { + if (opts.noglobstar === true) { + consume(value); + continue; + } + const prior = prev.prev; + const before = prior.prev; + const isStart = prior.type === "slash" || prior.type === "bos"; + const afterStar = before && (before.type === "star" || before.type === "globstar"); + if (opts.bash === true && (!isStart || rest[0] && rest[0] !== "/")) { + push({type: "star", value, output: ""}); + continue; + } + const isBrace = state.braces > 0 && (prior.type === "comma" || prior.type === "brace"); + const isExtglob = extglobs.length && (prior.type === "pipe" || prior.type === "paren"); + if (!isStart && prior.type !== "paren" && !isBrace && !isExtglob) { + push({type: "star", value, output: ""}); + continue; + } + while (rest.slice(0, 3) === "/**") { + const after = input[state.index + 4]; + if (after && after !== "/") { + break; + } + rest = rest.slice(3); + consume("/**", 3); + } + if (prior.type === "bos" && eos()) { + prev.type = "globstar"; + prev.value += value; + prev.output = globstar(opts); + state.output = prev.output; + state.globstar = true; + consume(value); + continue; + } + if (prior.type === "slash" && prior.prev.type !== "bos" && !afterStar && eos()) { + state.output = state.output.slice(0, -(prior.output + prev.output).length); + prior.output = `(?:${prior.output}`; + prev.type = "globstar"; + prev.output = globstar(opts) + (opts.strictSlashes ? ")" : "|$)"); + prev.value += value; + state.globstar = true; + state.output += prior.output + prev.output; + consume(value); + continue; + } + if (prior.type === "slash" && prior.prev.type !== "bos" && rest[0] === "/") { + const end = rest[1] !== void 0 ? "|$" : ""; + state.output = state.output.slice(0, -(prior.output + prev.output).length); + prior.output = `(?:${prior.output}`; + prev.type = "globstar"; + prev.output = `${globstar(opts)}${SLASH_LITERAL}|${SLASH_LITERAL}${end})`; + prev.value += value; + state.output += prior.output + prev.output; + state.globstar = true; + consume(value + advance()); + push({type: "slash", value: "/", output: ""}); + continue; + } + if (prior.type === "bos" && rest[0] === "/") { + prev.type = "globstar"; + prev.value += value; + prev.output = `(?:^|${SLASH_LITERAL}|${globstar(opts)}${SLASH_LITERAL})`; + state.output = prev.output; + state.globstar = true; + consume(value + advance()); + push({type: "slash", value: "/", output: ""}); + continue; + } + state.output = state.output.slice(0, -prev.output.length); + prev.type = "globstar"; + prev.output = globstar(opts); + prev.value += value; + state.output += prev.output; + state.globstar = true; + consume(value); + continue; + } + const token = {type: "star", value, output: star}; + if (opts.bash === true) { + token.output = ".*?"; + if (prev.type === "bos" || prev.type === "slash") { + token.output = nodot + token.output; + } + push(token); + continue; + } + if (prev && (prev.type === "bracket" || prev.type === "paren") && opts.regex === true) { + token.output = value; + push(token); + continue; + } + if (state.index === state.start || prev.type === "slash" || prev.type === "dot") { + if (prev.type === "dot") { + state.output += NO_DOT_SLASH; + prev.output += NO_DOT_SLASH; + } else if (opts.dot === true) { + state.output += NO_DOTS_SLASH; + prev.output += NO_DOTS_SLASH; + } else { + state.output += nodot; + prev.output += nodot; + } + if (peek() !== "*") { + state.output += ONE_CHAR; + prev.output += ONE_CHAR; + } + } + push(token); + } + while (state.brackets > 0) { + if (opts.strictBrackets === true) + throw new SyntaxError(syntaxError("closing", "]")); + state.output = utils.escapeLast(state.output, "["); + decrement("brackets"); + } + while (state.parens > 0) { + if (opts.strictBrackets === true) + throw new SyntaxError(syntaxError("closing", ")")); + state.output = utils.escapeLast(state.output, "("); + decrement("parens"); + } + while (state.braces > 0) { + if (opts.strictBrackets === true) + throw new SyntaxError(syntaxError("closing", "}")); + state.output = utils.escapeLast(state.output, "{"); + decrement("braces"); + } + if (opts.strictSlashes !== true && (prev.type === "star" || prev.type === "bracket")) { + push({type: "maybe_slash", value: "", output: `${SLASH_LITERAL}?`}); + } + if (state.backtrack === true) { + state.output = ""; + for (const token of state.tokens) { + state.output += token.output != null ? token.output : token.value; + if (token.suffix) { + state.output += token.suffix; + } + } + } + return state; + }; + parse.fastpaths = (input, options) => { + const opts = {...options}; + const max = typeof opts.maxLength === "number" ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; + const len = input.length; + if (len > max) { + throw new SyntaxError(`Input length: ${len}, exceeds maximum allowed length: ${max}`); + } + input = REPLACEMENTS[input] || input; + const win32 = utils.isWindows(options); + const { + DOT_LITERAL, + SLASH_LITERAL, + ONE_CHAR, + DOTS_SLASH, + NO_DOT, + NO_DOTS, + NO_DOTS_SLASH, + STAR, + START_ANCHOR + } = constants.globChars(win32); + const nodot = opts.dot ? NO_DOTS : NO_DOT; + const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT; + const capture = opts.capture ? "" : "?:"; + const state = {negated: false, prefix: ""}; + let star = opts.bash === true ? ".*?" : STAR; + if (opts.capture) { + star = `(${star})`; + } + const globstar = (opts2) => { + if (opts2.noglobstar === true) + return star; + return `(${capture}(?:(?!${START_ANCHOR}${opts2.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; + }; + const create = (str) => { + switch (str) { + case "*": + return `${nodot}${ONE_CHAR}${star}`; + case ".*": + return `${DOT_LITERAL}${ONE_CHAR}${star}`; + case "*.*": + return `${nodot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; + case "*/*": + return `${nodot}${star}${SLASH_LITERAL}${ONE_CHAR}${slashDot}${star}`; + case "**": + return nodot + globstar(opts); + case "**/*": + return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${ONE_CHAR}${star}`; + case "**/*.*": + return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`; + case "**/.*": + return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`; + default: { + const match = /^(.*?)\.(\w+)$/.exec(str); + if (!match) + return; + const source2 = create(match[1]); + if (!source2) + return; + return source2 + DOT_LITERAL + match[2]; + } + } + }; + const output = utils.removePrefix(input, state); + let source = create(output); + if (source && opts.strictSlashes !== true) { + source += `${SLASH_LITERAL}?`; + } + return source; + }; + module2.exports = parse; +}); + +// ../../node_modules/picomatch/lib/picomatch.js +var require_picomatch = __commonJS((exports2, module2) => { + "use strict"; + var path = require("path"); + var scan = require_scan(); + var parse = require_parse(); + var utils = require_utils(); + var constants = require_constants(); + var isObject = (val) => val && typeof val === "object" && !Array.isArray(val); + var picomatch = (glob, options, returnState = false) => { + if (Array.isArray(glob)) { + const fns = glob.map((input) => picomatch(input, options, returnState)); + const arrayMatcher = (str) => { + for (const isMatch of fns) { + const state2 = isMatch(str); + if (state2) + return state2; + } + return false; + }; + return arrayMatcher; + } + const isState = isObject(glob) && glob.tokens && glob.input; + if (glob === "" || typeof glob !== "string" && !isState) { + throw new TypeError("Expected pattern to be a non-empty string"); + } + const opts = options || {}; + const posix = utils.isWindows(options); + const regex = isState ? picomatch.compileRe(glob, options) : picomatch.makeRe(glob, options, false, true); + const state = regex.state; + delete regex.state; + let isIgnored = () => false; + if (opts.ignore) { + const ignoreOpts = {...options, ignore: null, onMatch: null, onResult: null}; + isIgnored = picomatch(opts.ignore, ignoreOpts, returnState); + } + const matcher = (input, returnObject = false) => { + const {isMatch, match, output} = picomatch.test(input, regex, options, {glob, posix}); + const result = {glob, state, regex, posix, input, output, match, isMatch}; + if (typeof opts.onResult === "function") { + opts.onResult(result); + } + if (isMatch === false) { + result.isMatch = false; + return returnObject ? result : false; + } + if (isIgnored(input)) { + if (typeof opts.onIgnore === "function") { + opts.onIgnore(result); + } + result.isMatch = false; + return returnObject ? result : false; + } + if (typeof opts.onMatch === "function") { + opts.onMatch(result); + } + return returnObject ? result : true; + }; + if (returnState) { + matcher.state = state; + } + return matcher; + }; + picomatch.test = (input, regex, options, {glob, posix} = {}) => { + if (typeof input !== "string") { + throw new TypeError("Expected input to be a string"); + } + if (input === "") { + return {isMatch: false, output: ""}; + } + const opts = options || {}; + const format = opts.format || (posix ? utils.toPosixSlashes : null); + let match = input === glob; + let output = match && format ? format(input) : input; + if (match === false) { + output = format ? format(input) : input; + match = output === glob; + } + if (match === false || opts.capture === true) { + if (opts.matchBase === true || opts.basename === true) { + match = picomatch.matchBase(input, regex, options, posix); + } else { + match = regex.exec(output); + } + } + return {isMatch: Boolean(match), match, output}; + }; + picomatch.matchBase = (input, glob, options, posix = utils.isWindows(options)) => { + const regex = glob instanceof RegExp ? glob : picomatch.makeRe(glob, options); + return regex.test(path.basename(input)); + }; + picomatch.isMatch = (str, patterns, options) => picomatch(patterns, options)(str); + picomatch.parse = (pattern, options) => { + if (Array.isArray(pattern)) + return pattern.map((p) => picomatch.parse(p, options)); + return parse(pattern, {...options, fastpaths: false}); + }; + picomatch.scan = (input, options) => scan(input, options); + picomatch.compileRe = (state, options, returnOutput = false, returnState = false) => { + if (returnOutput === true) { + return state.output; + } + const opts = options || {}; + const prepend = opts.contains ? "" : "^"; + const append = opts.contains ? "" : "$"; + let source = `${prepend}(?:${state.output})${append}`; + if (state && state.negated === true) { + source = `^(?!${source}).*$`; + } + const regex = picomatch.toRegex(source, options); + if (returnState === true) { + regex.state = state; + } + return regex; + }; + picomatch.makeRe = (input, options = {}, returnOutput = false, returnState = false) => { + if (!input || typeof input !== "string") { + throw new TypeError("Expected a non-empty string"); + } + let parsed = {negated: false, fastpaths: true}; + if (options.fastpaths !== false && (input[0] === "." || input[0] === "*")) { + parsed.output = parse.fastpaths(input, options); + } + if (!parsed.output) { + parsed = parse(input, options); + } + return picomatch.compileRe(parsed, options, returnOutput, returnState); + }; + picomatch.toRegex = (source, options) => { + try { + const opts = options || {}; + return new RegExp(source, opts.flags || (opts.nocase ? "i" : "")); + } catch (err) { + if (options && options.debug === true) + throw err; + return /$^/; + } + }; + picomatch.constants = constants; + module2.exports = picomatch; +}); + +// ../../node_modules/picomatch/index.js +var require_picomatch2 = __commonJS((exports2, module2) => { + "use strict"; + module2.exports = require_picomatch(); +}); + +// ../../node_modules/readdirp/index.js +var require_readdirp = __commonJS((exports2, module2) => { + "use strict"; + var fs = require("fs"); + var {Readable} = require("stream"); + var sysPath = require("path"); + var {promisify} = require("util"); + var picomatch = require_picomatch2(); + var readdir = promisify(fs.readdir); + var stat = promisify(fs.stat); + var lstat = promisify(fs.lstat); + var realpath = promisify(fs.realpath); + var BANG = "!"; + var RECURSIVE_ERROR_CODE = "READDIRP_RECURSIVE_ERROR"; + var NORMAL_FLOW_ERRORS = new Set(["ENOENT", "EPERM", "EACCES", "ELOOP", RECURSIVE_ERROR_CODE]); + var FILE_TYPE = "files"; + var DIR_TYPE = "directories"; + var FILE_DIR_TYPE = "files_directories"; + var EVERYTHING_TYPE = "all"; + var ALL_TYPES = [FILE_TYPE, DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE]; + var isNormalFlowError = (error) => NORMAL_FLOW_ERRORS.has(error.code); + var [maj, min] = process.versions.node.split(".").slice(0, 2).map((n) => Number.parseInt(n, 10)); + var wantBigintFsStats = process.platform === "win32" && (maj > 10 || maj === 10 && min >= 5); + var normalizeFilter = (filter) => { + if (filter === void 0) + return; + if (typeof filter === "function") + return filter; + if (typeof filter === "string") { + const glob = picomatch(filter.trim()); + return (entry) => glob(entry.basename); + } + if (Array.isArray(filter)) { + const positive = []; + const negative = []; + for (const item of filter) { + const trimmed = item.trim(); + if (trimmed.charAt(0) === BANG) { + negative.push(picomatch(trimmed.slice(1))); + } else { + positive.push(picomatch(trimmed)); + } + } + if (negative.length > 0) { + if (positive.length > 0) { + return (entry) => positive.some((f) => f(entry.basename)) && !negative.some((f) => f(entry.basename)); + } + return (entry) => !negative.some((f) => f(entry.basename)); + } + return (entry) => positive.some((f) => f(entry.basename)); + } + }; + var ReaddirpStream = class extends Readable { + static get defaultOptions() { + return { + root: ".", + fileFilter: (path) => true, + directoryFilter: (path) => true, + type: FILE_TYPE, + lstat: false, + depth: 2147483648, + alwaysStat: false + }; + } + constructor(options = {}) { + super({ + objectMode: true, + autoDestroy: true, + highWaterMark: options.highWaterMark || 4096 + }); + const opts = {...ReaddirpStream.defaultOptions, ...options}; + const {root, type} = opts; + this._fileFilter = normalizeFilter(opts.fileFilter); + this._directoryFilter = normalizeFilter(opts.directoryFilter); + const statMethod = opts.lstat ? lstat : stat; + if (wantBigintFsStats) { + this._stat = (path) => statMethod(path, {bigint: true}); + } else { + this._stat = statMethod; + } + this._maxDepth = opts.depth; + this._wantsDir = [DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type); + this._wantsFile = [FILE_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type); + this._wantsEverything = type === EVERYTHING_TYPE; + this._root = sysPath.resolve(root); + this._isDirent = "Dirent" in fs && !opts.alwaysStat; + this._statsProp = this._isDirent ? "dirent" : "stats"; + this._rdOptions = {encoding: "utf8", withFileTypes: this._isDirent}; + this.parents = [this._exploreDir(root, 1)]; + this.reading = false; + this.parent = void 0; + } + async _read(batch) { + if (this.reading) + return; + this.reading = true; + try { + while (!this.destroyed && batch > 0) { + const {path, depth, files = []} = this.parent || {}; + if (files.length > 0) { + const slice = files.splice(0, batch).map((dirent) => this._formatEntry(dirent, path)); + for (const entry of await Promise.all(slice)) { + if (this.destroyed) + return; + const entryType = await this._getEntryType(entry); + if (entryType === "directory" && this._directoryFilter(entry)) { + if (depth <= this._maxDepth) { + this.parents.push(this._exploreDir(entry.fullPath, depth + 1)); + } + if (this._wantsDir) { + this.push(entry); + batch--; + } + } else if ((entryType === "file" || this._includeAsFile(entry)) && this._fileFilter(entry)) { + if (this._wantsFile) { + this.push(entry); + batch--; + } + } + } + } else { + const parent = this.parents.pop(); + if (!parent) { + this.push(null); + break; + } + this.parent = await parent; + if (this.destroyed) + return; + } + } + } catch (error) { + this.destroy(error); + } finally { + this.reading = false; + } + } + async _exploreDir(path, depth) { + let files; + try { + files = await readdir(path, this._rdOptions); + } catch (error) { + this._onError(error); + } + return {files, depth, path}; + } + async _formatEntry(dirent, path) { + let entry; + try { + const basename = this._isDirent ? dirent.name : dirent; + const fullPath = sysPath.resolve(sysPath.join(path, basename)); + entry = {path: sysPath.relative(this._root, fullPath), fullPath, basename}; + entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath); + } catch (err) { + this._onError(err); + } + return entry; + } + _onError(err) { + if (isNormalFlowError(err) && !this.destroyed) { + this.emit("warn", err); + } else { + this.destroy(err); + } + } + async _getEntryType(entry) { + const stats = entry && entry[this._statsProp]; + if (!stats) { + return; + } + if (stats.isFile()) { + return "file"; + } + if (stats.isDirectory()) { + return "directory"; + } + if (stats && stats.isSymbolicLink()) { + const full = entry.fullPath; + try { + const entryRealPath = await realpath(full); + const entryRealPathStats = await lstat(entryRealPath); + if (entryRealPathStats.isFile()) { + return "file"; + } + if (entryRealPathStats.isDirectory()) { + const len = entryRealPath.length; + if (full.startsWith(entryRealPath) && full.substr(len, 1) === sysPath.sep) { + const recursiveError = new Error(`Circular symlink detected: "${full}" points to "${entryRealPath}"`); + recursiveError.code = RECURSIVE_ERROR_CODE; + return this._onError(recursiveError); + } + return "directory"; + } + } catch (error) { + this._onError(error); + } + } + } + _includeAsFile(entry) { + const stats = entry && entry[this._statsProp]; + return stats && this._wantsEverything && !stats.isDirectory(); + } + }; + var readdirp = (root, options = {}) => { + let type = options.entryType || options.type; + if (type === "both") + type = FILE_DIR_TYPE; + if (type) + options.type = type; + if (!root) { + throw new Error("readdirp: root argument is required. Usage: readdirp(root, options)"); + } else if (typeof root !== "string") { + throw new TypeError("readdirp: root argument must be a string. Usage: readdirp(root, options)"); + } else if (type && !ALL_TYPES.includes(type)) { + throw new Error(`readdirp: Invalid type passed. Use one of ${ALL_TYPES.join(", ")}`); + } + options.root = root; + return new ReaddirpStream(options); + }; + var readdirpPromise = (root, options = {}) => { + return new Promise((resolve, reject) => { + const files = []; + readdirp(root, options).on("data", (entry) => files.push(entry)).on("end", () => resolve(files)).on("error", (error) => reject(error)); + }); + }; + readdirp.promise = readdirpPromise; + readdirp.ReaddirpStream = ReaddirpStream; + readdirp.default = readdirp; + module2.exports = readdirp; +}); + +// ../../node_modules/normalize-path/index.js +var require_normalize_path = __commonJS((exports2, module2) => { + /*! + * normalize-path + * + * Copyright (c) 2014-2018, Jon Schlinkert. + * Released under the MIT License. + */ + module2.exports = function(path, stripTrailing) { + if (typeof path !== "string") { + throw new TypeError("expected path to be a string"); + } + if (path === "\\" || path === "/") + return "/"; + var len = path.length; + if (len <= 1) + return path; + var prefix = ""; + if (len > 4 && path[3] === "\\") { + var ch = path[2]; + if ((ch === "?" || ch === ".") && path.slice(0, 2) === "\\\\") { + path = path.slice(2); + prefix = "//"; + } + } + var segs = path.split(/[/\\]+/); + if (stripTrailing !== false && segs[segs.length - 1] === "") { + segs.pop(); + } + return prefix + segs.join("/"); + }; +}); + +// ../../node_modules/anymatch/index.js +var require_anymatch = __commonJS((exports2, module2) => { + "use strict"; + Object.defineProperty(exports2, "__esModule", {value: true}); + var picomatch = require_picomatch2(); + var normalizePath = require_normalize_path(); + var BANG = "!"; + var DEFAULT_OPTIONS = {returnIndex: false}; + var arrify = (item) => Array.isArray(item) ? item : [item]; + var createPattern = (matcher, options) => { + if (typeof matcher === "function") { + return matcher; + } + if (typeof matcher === "string") { + const glob = picomatch(matcher, options); + return (string) => matcher === string || glob(string); + } + if (matcher instanceof RegExp) { + return (string) => matcher.test(string); + } + return (string) => false; + }; + var matchPatterns = (patterns, negPatterns, args, returnIndex) => { + const isList = Array.isArray(args); + const _path = isList ? args[0] : args; + if (!isList && typeof _path !== "string") { + throw new TypeError("anymatch: second argument must be a string: got " + Object.prototype.toString.call(_path)); + } + const path = normalizePath(_path); + for (let index = 0; index < negPatterns.length; index++) { + const nglob = negPatterns[index]; + if (nglob(path)) { + return returnIndex ? -1 : false; + } + } + const applied = isList && [path].concat(args.slice(1)); + for (let index = 0; index < patterns.length; index++) { + const pattern = patterns[index]; + if (isList ? pattern(...applied) : pattern(path)) { + return returnIndex ? index : true; + } + } + return returnIndex ? -1 : false; + }; + var anymatch = (matchers, testString, options = DEFAULT_OPTIONS) => { + if (matchers == null) { + throw new TypeError("anymatch: specify first argument"); + } + const opts = typeof options === "boolean" ? {returnIndex: options} : options; + const returnIndex = opts.returnIndex || false; + const mtchers = arrify(matchers); + const negatedGlobs = mtchers.filter((item) => typeof item === "string" && item.charAt(0) === BANG).map((item) => item.slice(1)).map((item) => picomatch(item, opts)); + const patterns = mtchers.filter((item) => typeof item !== "string" || typeof item === "string" && item.charAt(0) !== BANG).map((matcher) => createPattern(matcher, opts)); + if (testString == null) { + return (testString2, ri = false) => { + const returnIndex2 = typeof ri === "boolean" ? ri : false; + return matchPatterns(patterns, negatedGlobs, testString2, returnIndex2); + }; + } + return matchPatterns(patterns, negatedGlobs, testString, returnIndex); + }; + anymatch.default = anymatch; + module2.exports = anymatch; +}); + +// ../../node_modules/is-extglob/index.js +var require_is_extglob = __commonJS((exports2, module2) => { + /*! + * is-extglob + * + * Copyright (c) 2014-2016, Jon Schlinkert. + * Licensed under the MIT License. + */ + module2.exports = function isExtglob(str) { + if (typeof str !== "string" || str === "") { + return false; + } + var match; + while (match = /(\\).|([@?!+*]\(.*\))/g.exec(str)) { + if (match[2]) + return true; + str = str.slice(match.index + match[0].length); + } + return false; + }; +}); + +// ../../node_modules/is-glob/index.js +var require_is_glob = __commonJS((exports2, module2) => { + /*! + * is-glob + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + var isExtglob = require_is_extglob(); + var chars = {"{": "}", "(": ")", "[": "]"}; + var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; + var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; + module2.exports = function isGlob(str, options) { + if (typeof str !== "string" || str === "") { + return false; + } + if (isExtglob(str)) { + return true; + } + var regex = strictRegex; + var match; + if (options && options.strict === false) { + regex = relaxedRegex; + } + while (match = regex.exec(str)) { + if (match[2]) + return true; + var idx = match.index + match[0].length; + var open = match[1]; + var close = open ? chars[open] : null; + if (open && close) { + var n = str.indexOf(close, idx); + if (n !== -1) { + idx = n + 1; + } + } + str = str.slice(idx); + } + return false; + }; +}); + +// ../../node_modules/glob-parent/index.js +var require_glob_parent = __commonJS((exports2, module2) => { + "use strict"; + var isGlob = require_is_glob(); + var pathPosixDirname = require("path").posix.dirname; + var isWin32 = require("os").platform() === "win32"; + var slash = "/"; + var backslash = /\\/g; + var enclosure = /[\{\[].*[\}\]]$/; + var globby = /(^|[^\\])([\{\[]|\([^\)]+$)/; + var escaped = /\\([\!\*\?\|\[\]\(\)\{\}])/g; + module2.exports = function globParent(str, opts) { + var options = Object.assign({flipBackslashes: true}, opts); + if (options.flipBackslashes && isWin32 && str.indexOf(slash) < 0) { + str = str.replace(backslash, slash); + } + if (enclosure.test(str)) { + str += slash; + } + str += "a"; + do { + str = pathPosixDirname(str); + } while (isGlob(str) || globby.test(str)); + return str.replace(escaped, "$1"); + }; +}); + +// ../../node_modules/chokidar/node_modules/braces/lib/utils.js +var require_utils2 = __commonJS((exports2) => { + "use strict"; + exports2.isInteger = (num) => { + if (typeof num === "number") { + return Number.isInteger(num); + } + if (typeof num === "string" && num.trim() !== "") { + return Number.isInteger(Number(num)); + } + return false; + }; + exports2.find = (node, type) => node.nodes.find((node2) => node2.type === type); + exports2.exceedsLimit = (min, max, step = 1, limit) => { + if (limit === false) + return false; + if (!exports2.isInteger(min) || !exports2.isInteger(max)) + return false; + return (Number(max) - Number(min)) / Number(step) >= limit; + }; + exports2.escapeNode = (block, n = 0, type) => { + let node = block.nodes[n]; + if (!node) + return; + if (type && node.type === type || node.type === "open" || node.type === "close") { + if (node.escaped !== true) { + node.value = "\\" + node.value; + node.escaped = true; + } + } + }; + exports2.encloseBrace = (node) => { + if (node.type !== "brace") + return false; + if (node.commas >> 0 + node.ranges >> 0 === 0) { + node.invalid = true; + return true; + } + return false; + }; + exports2.isInvalidBrace = (block) => { + if (block.type !== "brace") + return false; + if (block.invalid === true || block.dollar) + return true; + if (block.commas >> 0 + block.ranges >> 0 === 0) { + block.invalid = true; + return true; + } + if (block.open !== true || block.close !== true) { + block.invalid = true; + return true; + } + return false; + }; + exports2.isOpenOrClose = (node) => { + if (node.type === "open" || node.type === "close") { + return true; + } + return node.open === true || node.close === true; + }; + exports2.reduce = (nodes2) => nodes2.reduce((acc, node) => { + if (node.type === "text") + acc.push(node.value); + if (node.type === "range") + node.type = "text"; + return acc; + }, []); + exports2.flatten = (...args) => { + const result = []; + const flat = (arr) => { + for (let i = 0; i < arr.length; i++) { + let ele = arr[i]; + Array.isArray(ele) ? flat(ele, result) : ele !== void 0 && result.push(ele); + } + return result; + }; + flat(args); + return result; + }; +}); + +// ../../node_modules/chokidar/node_modules/braces/lib/stringify.js +var require_stringify = __commonJS((exports2, module2) => { + "use strict"; + var utils = require_utils2(); + module2.exports = (ast, options = {}) => { + let stringify = (node, parent = {}) => { + let invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let output = ""; + if (node.value) { + if ((invalidBlock || invalidNode) && utils.isOpenOrClose(node)) { + return "\\" + node.value; + } + return node.value; + } + if (node.value) { + return node.value; + } + if (node.nodes) { + for (let child of node.nodes) { + output += stringify(child); + } + } + return output; + }; + return stringify(ast); + }; +}); + +// ../../node_modules/chokidar/node_modules/is-number/index.js +var require_is_number = __commonJS((exports2, module2) => { + /*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ + "use strict"; + module2.exports = function(num) { + if (typeof num === "number") { + return num - num === 0; + } + if (typeof num === "string" && num.trim() !== "") { + return Number.isFinite ? Number.isFinite(+num) : isFinite(+num); + } + return false; + }; +}); + +// ../../node_modules/chokidar/node_modules/to-regex-range/index.js +var require_to_regex_range = __commonJS((exports2, module2) => { + /*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ + "use strict"; + var isNumber = require_is_number(); + var toRegexRange = (min, max, options) => { + if (isNumber(min) === false) { + throw new TypeError("toRegexRange: expected the first argument to be a number"); + } + if (max === void 0 || min === max) { + return String(min); + } + if (isNumber(max) === false) { + throw new TypeError("toRegexRange: expected the second argument to be a number."); + } + let opts = {relaxZeros: true, ...options}; + if (typeof opts.strictZeros === "boolean") { + opts.relaxZeros = opts.strictZeros === false; + } + let relax = String(opts.relaxZeros); + let shorthand = String(opts.shorthand); + let capture = String(opts.capture); + let wrap = String(opts.wrap); + let cacheKey = min + ":" + max + "=" + relax + shorthand + capture + wrap; + if (toRegexRange.cache.hasOwnProperty(cacheKey)) { + return toRegexRange.cache[cacheKey].result; + } + let a = Math.min(min, max); + let b = Math.max(min, max); + if (Math.abs(a - b) === 1) { + let result = min + "|" + max; + if (opts.capture) { + return `(${result})`; + } + if (opts.wrap === false) { + return result; + } + return `(?:${result})`; + } + let isPadded = hasPadding(min) || hasPadding(max); + let state = {min, max, a, b}; + let positives = []; + let negatives = []; + if (isPadded) { + state.isPadded = isPadded; + state.maxLen = String(state.max).length; + } + if (a < 0) { + let newMin = b < 0 ? Math.abs(b) : 1; + negatives = splitToPatterns(newMin, Math.abs(a), state, opts); + a = state.a = 0; + } + if (b >= 0) { + positives = splitToPatterns(a, b, state, opts); + } + state.negatives = negatives; + state.positives = positives; + state.result = collatePatterns(negatives, positives, opts); + if (opts.capture === true) { + state.result = `(${state.result})`; + } else if (opts.wrap !== false && positives.length + negatives.length > 1) { + state.result = `(?:${state.result})`; + } + toRegexRange.cache[cacheKey] = state; + return state.result; + }; + function collatePatterns(neg, pos, options) { + let onlyNegative = filterPatterns(neg, pos, "-", false, options) || []; + let onlyPositive = filterPatterns(pos, neg, "", false, options) || []; + let intersected = filterPatterns(neg, pos, "-?", true, options) || []; + let subpatterns = onlyNegative.concat(intersected).concat(onlyPositive); + return subpatterns.join("|"); + } + function splitToRanges(min, max) { + let nines = 1; + let zeros = 1; + let stop = countNines(min, nines); + let stops = new Set([max]); + while (min <= stop && stop <= max) { + stops.add(stop); + nines += 1; + stop = countNines(min, nines); + } + stop = countZeros(max + 1, zeros) - 1; + while (min < stop && stop <= max) { + stops.add(stop); + zeros += 1; + stop = countZeros(max + 1, zeros) - 1; + } + stops = [...stops]; + stops.sort(compare); + return stops; + } + function rangeToPattern(start, stop, options) { + if (start === stop) { + return {pattern: start, count: [], digits: 0}; + } + let zipped = zip(start, stop); + let digits = zipped.length; + let pattern = ""; + let count = 0; + for (let i = 0; i < digits; i++) { + let [startDigit, stopDigit] = zipped[i]; + if (startDigit === stopDigit) { + pattern += startDigit; + } else if (startDigit !== "0" || stopDigit !== "9") { + pattern += toCharacterClass(startDigit, stopDigit, options); + } else { + count++; + } + } + if (count) { + pattern += options.shorthand === true ? "\\d" : "[0-9]"; + } + return {pattern, count: [count], digits}; + } + function splitToPatterns(min, max, tok, options) { + let ranges = splitToRanges(min, max); + let tokens = []; + let start = min; + let prev; + for (let i = 0; i < ranges.length; i++) { + let max2 = ranges[i]; + let obj = rangeToPattern(String(start), String(max2), options); + let zeros = ""; + if (!tok.isPadded && prev && prev.pattern === obj.pattern) { + if (prev.count.length > 1) { + prev.count.pop(); + } + prev.count.push(obj.count[0]); + prev.string = prev.pattern + toQuantifier(prev.count); + start = max2 + 1; + continue; + } + if (tok.isPadded) { + zeros = padZeros(max2, tok, options); + } + obj.string = zeros + obj.pattern + toQuantifier(obj.count); + tokens.push(obj); + start = max2 + 1; + prev = obj; + } + return tokens; + } + function filterPatterns(arr, comparison, prefix, intersection, options) { + let result = []; + for (let ele of arr) { + let {string} = ele; + if (!intersection && !contains(comparison, "string", string)) { + result.push(prefix + string); + } + if (intersection && contains(comparison, "string", string)) { + result.push(prefix + string); + } + } + return result; + } + function zip(a, b) { + let arr = []; + for (let i = 0; i < a.length; i++) + arr.push([a[i], b[i]]); + return arr; + } + function compare(a, b) { + return a > b ? 1 : b > a ? -1 : 0; + } + function contains(arr, key, val) { + return arr.some((ele) => ele[key] === val); + } + function countNines(min, len) { + return Number(String(min).slice(0, -len) + "9".repeat(len)); + } + function countZeros(integer, zeros) { + return integer - integer % Math.pow(10, zeros); + } + function toQuantifier(digits) { + let [start = 0, stop = ""] = digits; + if (stop || start > 1) { + return `{${start + (stop ? "," + stop : "")}}`; + } + return ""; + } + function toCharacterClass(a, b, options) { + return `[${a}${b - a === 1 ? "" : "-"}${b}]`; + } + function hasPadding(str) { + return /^-?(0+)\d/.test(str); + } + function padZeros(value, tok, options) { + if (!tok.isPadded) { + return value; + } + let diff = Math.abs(tok.maxLen - String(value).length); + let relax = options.relaxZeros !== false; + switch (diff) { + case 0: + return ""; + case 1: + return relax ? "0?" : "0"; + case 2: + return relax ? "0{0,2}" : "00"; + default: { + return relax ? `0{0,${diff}}` : `0{${diff}}`; + } + } + } + toRegexRange.cache = {}; + toRegexRange.clearCache = () => toRegexRange.cache = {}; + module2.exports = toRegexRange; +}); + +// ../../node_modules/chokidar/node_modules/fill-range/index.js +var require_fill_range = __commonJS((exports2, module2) => { + /*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ + "use strict"; + var util = require("util"); + var toRegexRange = require_to_regex_range(); + var isObject = (val) => val !== null && typeof val === "object" && !Array.isArray(val); + var transform = (toNumber) => { + return (value) => toNumber === true ? Number(value) : String(value); + }; + var isValidValue = (value) => { + return typeof value === "number" || typeof value === "string" && value !== ""; + }; + var isNumber = (num) => Number.isInteger(+num); + var zeros = (input) => { + let value = `${input}`; + let index = -1; + if (value[0] === "-") + value = value.slice(1); + if (value === "0") + return false; + while (value[++index] === "0") + ; + return index > 0; + }; + var stringify = (start, end, options) => { + if (typeof start === "string" || typeof end === "string") { + return true; + } + return options.stringify === true; + }; + var pad = (input, maxLength, toNumber) => { + if (maxLength > 0) { + let dash = input[0] === "-" ? "-" : ""; + if (dash) + input = input.slice(1); + input = dash + input.padStart(dash ? maxLength - 1 : maxLength, "0"); + } + if (toNumber === false) { + return String(input); + } + return input; + }; + var toMaxLen = (input, maxLength) => { + let negative = input[0] === "-" ? "-" : ""; + if (negative) { + input = input.slice(1); + maxLength--; + } + while (input.length < maxLength) + input = "0" + input; + return negative ? "-" + input : input; + }; + var toSequence = (parts, options) => { + parts.negatives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + parts.positives.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); + let prefix = options.capture ? "" : "?:"; + let positives = ""; + let negatives = ""; + let result; + if (parts.positives.length) { + positives = parts.positives.join("|"); + } + if (parts.negatives.length) { + negatives = `-(${prefix}${parts.negatives.join("|")})`; + } + if (positives && negatives) { + result = `${positives}|${negatives}`; + } else { + result = positives || negatives; + } + if (options.wrap) { + return `(${prefix}${result})`; + } + return result; + }; + var toRange = (a, b, isNumbers, options) => { + if (isNumbers) { + return toRegexRange(a, b, {wrap: false, ...options}); + } + let start = String.fromCharCode(a); + if (a === b) + return start; + let stop = String.fromCharCode(b); + return `[${start}-${stop}]`; + }; + var toRegex = (start, end, options) => { + if (Array.isArray(start)) { + let wrap = options.wrap === true; + let prefix = options.capture ? "" : "?:"; + return wrap ? `(${prefix}${start.join("|")})` : start.join("|"); + } + return toRegexRange(start, end, options); + }; + var rangeError = (...args) => { + return new RangeError("Invalid range arguments: " + util.inspect(...args)); + }; + var invalidRange = (start, end, options) => { + if (options.strictRanges === true) + throw rangeError([start, end]); + return []; + }; + var invalidStep = (step, options) => { + if (options.strictRanges === true) { + throw new TypeError(`Expected step "${step}" to be a number`); + } + return []; + }; + var fillNumbers = (start, end, step = 1, options = {}) => { + let a = Number(start); + let b = Number(end); + if (!Number.isInteger(a) || !Number.isInteger(b)) { + if (options.strictRanges === true) + throw rangeError([start, end]); + return []; + } + if (a === 0) + a = 0; + if (b === 0) + b = 0; + let descending = a > b; + let startString = String(start); + let endString = String(end); + let stepString = String(step); + step = Math.max(Math.abs(step), 1); + let padded = zeros(startString) || zeros(endString) || zeros(stepString); + let maxLen = padded ? Math.max(startString.length, endString.length, stepString.length) : 0; + let toNumber = padded === false && stringify(start, end, options) === false; + let format = options.transform || transform(toNumber); + if (options.toRegex && step === 1) { + return toRange(toMaxLen(start, maxLen), toMaxLen(end, maxLen), true, options); + } + let parts = {negatives: [], positives: []}; + let push = (num) => parts[num < 0 ? "negatives" : "positives"].push(Math.abs(num)); + let range = []; + let index = 0; + while (descending ? a >= b : a <= b) { + if (options.toRegex === true && step > 1) { + push(a); + } else { + range.push(pad(format(a, index), maxLen, toNumber)); + } + a = descending ? a - step : a + step; + index++; + } + if (options.toRegex === true) { + return step > 1 ? toSequence(parts, options) : toRegex(range, null, {wrap: false, ...options}); + } + return range; + }; + var fillLetters = (start, end, step = 1, options = {}) => { + if (!isNumber(start) && start.length > 1 || !isNumber(end) && end.length > 1) { + return invalidRange(start, end, options); + } + let format = options.transform || ((val) => String.fromCharCode(val)); + let a = `${start}`.charCodeAt(0); + let b = `${end}`.charCodeAt(0); + let descending = a > b; + let min = Math.min(a, b); + let max = Math.max(a, b); + if (options.toRegex && step === 1) { + return toRange(min, max, false, options); + } + let range = []; + let index = 0; + while (descending ? a >= b : a <= b) { + range.push(format(a, index)); + a = descending ? a - step : a + step; + index++; + } + if (options.toRegex === true) { + return toRegex(range, null, {wrap: false, options}); + } + return range; + }; + var fill = (start, end, step, options = {}) => { + if (end == null && isValidValue(start)) { + return [start]; + } + if (!isValidValue(start) || !isValidValue(end)) { + return invalidRange(start, end, options); + } + if (typeof step === "function") { + return fill(start, end, 1, {transform: step}); + } + if (isObject(step)) { + return fill(start, end, 0, step); + } + let opts = {...options}; + if (opts.capture === true) + opts.wrap = true; + step = step || opts.step || 1; + if (!isNumber(step)) { + if (step != null && !isObject(step)) + return invalidStep(step, opts); + return fill(start, end, 1, step); + } + if (isNumber(start) && isNumber(end)) { + return fillNumbers(start, end, step, opts); + } + return fillLetters(start, end, Math.max(Math.abs(step), 1), opts); + }; + module2.exports = fill; +}); + +// ../../node_modules/chokidar/node_modules/braces/lib/compile.js +var require_compile = __commonJS((exports2, module2) => { + "use strict"; + var fill = require_fill_range(); + var utils = require_utils2(); + var compile2 = (ast, options = {}) => { + let walk = (node, parent = {}) => { + let invalidBlock = utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; + let invalid = invalidBlock === true || invalidNode === true; + let prefix = options.escapeInvalid === true ? "\\" : ""; + let output = ""; + if (node.isOpen === true) { + return prefix + node.value; + } + if (node.isClose === true) { + return prefix + node.value; + } + if (node.type === "open") { + return invalid ? prefix + node.value : "("; + } + if (node.type === "close") { + return invalid ? prefix + node.value : ")"; + } + if (node.type === "comma") { + return node.prev.type === "comma" ? "" : invalid ? node.value : "|"; + } + if (node.value) { + return node.value; + } + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + let range = fill(...args, {...options, wrap: false, toRegex: true}); + if (range.length !== 0) { + return args.length > 1 && range.length > 1 ? `(${range})` : range; + } + } + if (node.nodes) { + for (let child of node.nodes) { + output += walk(child, node); + } + } + return output; + }; + return walk(ast); + }; + module2.exports = compile2; +}); + +// ../../node_modules/chokidar/node_modules/braces/lib/expand.js +var require_expand = __commonJS((exports2, module2) => { + "use strict"; + var fill = require_fill_range(); + var stringify = require_stringify(); + var utils = require_utils2(); + var append = (queue = "", stash = "", enclose = false) => { + let result = []; + queue = [].concat(queue); + stash = [].concat(stash); + if (!stash.length) + return queue; + if (!queue.length) { + return enclose ? utils.flatten(stash).map((ele) => `{${ele}}`) : stash; + } + for (let item of queue) { + if (Array.isArray(item)) { + for (let value of item) { + result.push(append(value, stash, enclose)); + } + } else { + for (let ele of stash) { + if (enclose === true && typeof ele === "string") + ele = `{${ele}}`; + result.push(Array.isArray(ele) ? append(item, ele, enclose) : item + ele); + } + } + } + return utils.flatten(result); + }; + var expand = (ast, options = {}) => { + let rangeLimit = options.rangeLimit === void 0 ? 1e3 : options.rangeLimit; + let walk = (node, parent = {}) => { + node.queue = []; + let p = parent; + let q = parent.queue; + while (p.type !== "brace" && p.type !== "root" && p.parent) { + p = p.parent; + q = p.queue; + } + if (node.invalid || node.dollar) { + q.push(append(q.pop(), stringify(node, options))); + return; + } + if (node.type === "brace" && node.invalid !== true && node.nodes.length === 2) { + q.push(append(q.pop(), ["{}"])); + return; + } + if (node.nodes && node.ranges > 0) { + let args = utils.reduce(node.nodes); + if (utils.exceedsLimit(...args, options.step, rangeLimit)) { + throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit."); + } + let range = fill(...args, options); + if (range.length === 0) { + range = stringify(node, options); + } + q.push(append(q.pop(), range)); + node.nodes = []; + return; + } + let enclose = utils.encloseBrace(node); + let queue = node.queue; + let block = node; + while (block.type !== "brace" && block.type !== "root" && block.parent) { + block = block.parent; + queue = block.queue; + } + for (let i = 0; i < node.nodes.length; i++) { + let child = node.nodes[i]; + if (child.type === "comma" && node.type === "brace") { + if (i === 1) + queue.push(""); + queue.push(""); + continue; + } + if (child.type === "close") { + q.push(append(q.pop(), queue, enclose)); + continue; + } + if (child.value && child.type !== "open") { + queue.push(append(queue.pop(), child.value)); + continue; + } + if (child.nodes) { + walk(child, node); + } + } + return queue; + }; + return utils.flatten(walk(ast)); + }; + module2.exports = expand; +}); + +// ../../node_modules/chokidar/node_modules/braces/lib/constants.js +var require_constants2 = __commonJS((exports2, module2) => { + "use strict"; + module2.exports = { + MAX_LENGTH: 1024 * 64, + CHAR_0: "0", + CHAR_9: "9", + CHAR_UPPERCASE_A: "A", + CHAR_LOWERCASE_A: "a", + CHAR_UPPERCASE_Z: "Z", + CHAR_LOWERCASE_Z: "z", + CHAR_LEFT_PARENTHESES: "(", + CHAR_RIGHT_PARENTHESES: ")", + CHAR_ASTERISK: "*", + CHAR_AMPERSAND: "&", + CHAR_AT: "@", + CHAR_BACKSLASH: "\\", + CHAR_BACKTICK: "`", + CHAR_CARRIAGE_RETURN: "\r", + CHAR_CIRCUMFLEX_ACCENT: "^", + CHAR_COLON: ":", + CHAR_COMMA: ",", + CHAR_DOLLAR: "$", + CHAR_DOT: ".", + CHAR_DOUBLE_QUOTE: '"', + CHAR_EQUAL: "=", + CHAR_EXCLAMATION_MARK: "!", + CHAR_FORM_FEED: "\f", + CHAR_FORWARD_SLASH: "/", + CHAR_HASH: "#", + CHAR_HYPHEN_MINUS: "-", + CHAR_LEFT_ANGLE_BRACKET: "<", + CHAR_LEFT_CURLY_BRACE: "{", + CHAR_LEFT_SQUARE_BRACKET: "[", + CHAR_LINE_FEED: "\n", + CHAR_NO_BREAK_SPACE: "\xA0", + CHAR_PERCENT: "%", + CHAR_PLUS: "+", + CHAR_QUESTION_MARK: "?", + CHAR_RIGHT_ANGLE_BRACKET: ">", + CHAR_RIGHT_CURLY_BRACE: "}", + CHAR_RIGHT_SQUARE_BRACKET: "]", + CHAR_SEMICOLON: ";", + CHAR_SINGLE_QUOTE: "'", + CHAR_SPACE: " ", + CHAR_TAB: " ", + CHAR_UNDERSCORE: "_", + CHAR_VERTICAL_LINE: "|", + CHAR_ZERO_WIDTH_NOBREAK_SPACE: "\uFEFF" + }; +}); + +// ../../node_modules/chokidar/node_modules/braces/lib/parse.js +var require_parse2 = __commonJS((exports2, module2) => { + "use strict"; + var stringify = require_stringify(); + var { + MAX_LENGTH, + CHAR_BACKSLASH, + CHAR_BACKTICK, + CHAR_COMMA, + CHAR_DOT, + CHAR_LEFT_PARENTHESES, + CHAR_RIGHT_PARENTHESES, + CHAR_LEFT_CURLY_BRACE, + CHAR_RIGHT_CURLY_BRACE, + CHAR_LEFT_SQUARE_BRACKET, + CHAR_RIGHT_SQUARE_BRACKET, + CHAR_DOUBLE_QUOTE, + CHAR_SINGLE_QUOTE, + CHAR_NO_BREAK_SPACE, + CHAR_ZERO_WIDTH_NOBREAK_SPACE + } = require_constants2(); + var parse = (input, options = {}) => { + if (typeof input !== "string") { + throw new TypeError("Expected a string"); + } + let opts = options || {}; + let max = typeof opts.maxLength === "number" ? Math.min(MAX_LENGTH, opts.maxLength) : MAX_LENGTH; + if (input.length > max) { + throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`); + } + let ast = {type: "root", input, nodes: []}; + let stack = [ast]; + let block = ast; + let prev = ast; + let brackets = 0; + let length = input.length; + let index = 0; + let depth = 0; + let value; + let memo = {}; + const advance = () => input[index++]; + const push = (node) => { + if (node.type === "text" && prev.type === "dot") { + prev.type = "text"; + } + if (prev && prev.type === "text" && node.type === "text") { + prev.value += node.value; + return; + } + block.nodes.push(node); + node.parent = block; + node.prev = prev; + prev = node; + return node; + }; + push({type: "bos"}); + while (index < length) { + block = stack[stack.length - 1]; + value = advance(); + if (value === CHAR_ZERO_WIDTH_NOBREAK_SPACE || value === CHAR_NO_BREAK_SPACE) { + continue; + } + if (value === CHAR_BACKSLASH) { + push({type: "text", value: (options.keepEscaping ? value : "") + advance()}); + continue; + } + if (value === CHAR_RIGHT_SQUARE_BRACKET) { + push({type: "text", value: "\\" + value}); + continue; + } + if (value === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + let closed = true; + let next; + while (index < length && (next = advance())) { + value += next; + if (next === CHAR_LEFT_SQUARE_BRACKET) { + brackets++; + continue; + } + if (next === CHAR_BACKSLASH) { + value += advance(); + continue; + } + if (next === CHAR_RIGHT_SQUARE_BRACKET) { + brackets--; + if (brackets === 0) { + break; + } + } + } + push({type: "text", value}); + continue; + } + if (value === CHAR_LEFT_PARENTHESES) { + block = push({type: "paren", nodes: []}); + stack.push(block); + push({type: "text", value}); + continue; + } + if (value === CHAR_RIGHT_PARENTHESES) { + if (block.type !== "paren") { + push({type: "text", value}); + continue; + } + block = stack.pop(); + push({type: "text", value}); + block = stack[stack.length - 1]; + continue; + } + if (value === CHAR_DOUBLE_QUOTE || value === CHAR_SINGLE_QUOTE || value === CHAR_BACKTICK) { + let open = value; + let next; + if (options.keepQuotes !== true) { + value = ""; + } + while (index < length && (next = advance())) { + if (next === CHAR_BACKSLASH) { + value += next + advance(); + continue; + } + if (next === open) { + if (options.keepQuotes === true) + value += next; + break; + } + value += next; + } + push({type: "text", value}); + continue; + } + if (value === CHAR_LEFT_CURLY_BRACE) { + depth++; + let dollar = prev.value && prev.value.slice(-1) === "$" || block.dollar === true; + let brace = { + type: "brace", + open: true, + close: false, + dollar, + depth, + commas: 0, + ranges: 0, + nodes: [] + }; + block = push(brace); + stack.push(block); + push({type: "open", value}); + continue; + } + if (value === CHAR_RIGHT_CURLY_BRACE) { + if (block.type !== "brace") { + push({type: "text", value}); + continue; + } + let type = "close"; + block = stack.pop(); + block.close = true; + push({type, value}); + depth--; + block = stack[stack.length - 1]; + continue; + } + if (value === CHAR_COMMA && depth > 0) { + if (block.ranges > 0) { + block.ranges = 0; + let open = block.nodes.shift(); + block.nodes = [open, {type: "text", value: stringify(block)}]; + } + push({type: "comma", value}); + block.commas++; + continue; + } + if (value === CHAR_DOT && depth > 0 && block.commas === 0) { + let siblings = block.nodes; + if (depth === 0 || siblings.length === 0) { + push({type: "text", value}); + continue; + } + if (prev.type === "dot") { + block.range = []; + prev.value += value; + prev.type = "range"; + if (block.nodes.length !== 3 && block.nodes.length !== 5) { + block.invalid = true; + block.ranges = 0; + prev.type = "text"; + continue; + } + block.ranges++; + block.args = []; + continue; + } + if (prev.type === "range") { + siblings.pop(); + let before = siblings[siblings.length - 1]; + before.value += prev.value + value; + prev = before; + block.ranges--; + continue; + } + push({type: "dot", value}); + continue; + } + push({type: "text", value}); + } + do { + block = stack.pop(); + if (block.type !== "root") { + block.nodes.forEach((node) => { + if (!node.nodes) { + if (node.type === "open") + node.isOpen = true; + if (node.type === "close") + node.isClose = true; + if (!node.nodes) + node.type = "text"; + node.invalid = true; + } + }); + let parent = stack[stack.length - 1]; + let index2 = parent.nodes.indexOf(block); + parent.nodes.splice(index2, 1, ...block.nodes); + } + } while (stack.length > 0); + push({type: "eos"}); + return ast; + }; + module2.exports = parse; +}); + +// ../../node_modules/chokidar/node_modules/braces/index.js +var require_braces = __commonJS((exports2, module2) => { + "use strict"; + var stringify = require_stringify(); + var compile2 = require_compile(); + var expand = require_expand(); + var parse = require_parse2(); + var braces = (input, options = {}) => { + let output = []; + if (Array.isArray(input)) { + for (let pattern of input) { + let result = braces.create(pattern, options); + if (Array.isArray(result)) { + output.push(...result); + } else { + output.push(result); + } + } + } else { + output = [].concat(braces.create(input, options)); + } + if (options && options.expand === true && options.nodupes === true) { + output = [...new Set(output)]; + } + return output; + }; + braces.parse = (input, options = {}) => parse(input, options); + braces.stringify = (input, options = {}) => { + if (typeof input === "string") { + return stringify(braces.parse(input, options), options); + } + return stringify(input, options); + }; + braces.compile = (input, options = {}) => { + if (typeof input === "string") { + input = braces.parse(input, options); + } + return compile2(input, options); + }; + braces.expand = (input, options = {}) => { + if (typeof input === "string") { + input = braces.parse(input, options); + } + let result = expand(input, options); + if (options.noempty === true) { + result = result.filter(Boolean); + } + if (options.nodupes === true) { + result = [...new Set(result)]; + } + return result; + }; + braces.create = (input, options = {}) => { + if (input === "" || input.length < 3) { + return [input]; + } + return options.expand !== true ? braces.compile(input, options) : braces.expand(input, options); + }; + module2.exports = braces; +}); + +// ../../node_modules/binary-extensions/binary-extensions.json +var require_binary_extensions = __commonJS((exports2, module2) => { + module2.exports = [ + "3dm", + "3ds", + "3g2", + "3gp", + "7z", + "a", + "aac", + "adp", + "ai", + "aif", + "aiff", + "alz", + "ape", + "apk", + "ar", + "arj", + "asf", + "au", + "avi", + "bak", + "baml", + "bh", + "bin", + "bk", + "bmp", + "btif", + "bz2", + "bzip2", + "cab", + "caf", + "cgm", + "class", + "cmx", + "cpio", + "cr2", + "cur", + "dat", + "dcm", + "deb", + "dex", + "djvu", + "dll", + "dmg", + "dng", + "doc", + "docm", + "docx", + "dot", + "dotm", + "dra", + "DS_Store", + "dsk", + "dts", + "dtshd", + "dvb", + "dwg", + "dxf", + "ecelp4800", + "ecelp7470", + "ecelp9600", + "egg", + "eol", + "eot", + "epub", + "exe", + "f4v", + "fbs", + "fh", + "fla", + "flac", + "fli", + "flv", + "fpx", + "fst", + "fvt", + "g3", + "gh", + "gif", + "graffle", + "gz", + "gzip", + "h261", + "h263", + "h264", + "icns", + "ico", + "ief", + "img", + "ipa", + "iso", + "jar", + "jpeg", + "jpg", + "jpgv", + "jpm", + "jxr", + "key", + "ktx", + "lha", + "lib", + "lvp", + "lz", + "lzh", + "lzma", + "lzo", + "m3u", + "m4a", + "m4v", + "mar", + "mdi", + "mht", + "mid", + "midi", + "mj2", + "mka", + "mkv", + "mmr", + "mng", + "mobi", + "mov", + "movie", + "mp3", + "mp4", + "mp4a", + "mpeg", + "mpg", + "mpga", + "mxu", + "nef", + "npx", + "numbers", + "nupkg", + "o", + "oga", + "ogg", + "ogv", + "otf", + "pages", + "pbm", + "pcx", + "pdb", + "pdf", + "pea", + "pgm", + "pic", + "png", + "pnm", + "pot", + "potm", + "potx", + "ppa", + "ppam", + "ppm", + "pps", + "ppsm", + "ppsx", + "ppt", + "pptm", + "pptx", + "psd", + "pya", + "pyc", + "pyo", + "pyv", + "qt", + "rar", + "ras", + "raw", + "resources", + "rgb", + "rip", + "rlc", + "rmf", + "rmvb", + "rtf", + "rz", + "s3m", + "s7z", + "scpt", + "sgi", + "shar", + "sil", + "sketch", + "slk", + "smv", + "snk", + "so", + "stl", + "suo", + "sub", + "swf", + "tar", + "tbz", + "tbz2", + "tga", + "tgz", + "thmx", + "tif", + "tiff", + "tlz", + "ttc", + "ttf", + "txz", + "udf", + "uvh", + "uvi", + "uvm", + "uvp", + "uvs", + "uvu", + "viv", + "vob", + "war", + "wav", + "wax", + "wbmp", + "wdp", + "weba", + "webm", + "webp", + "whl", + "wim", + "wm", + "wma", + "wmv", + "wmx", + "woff", + "woff2", + "wrm", + "wvx", + "xbm", + "xif", + "xla", + "xlam", + "xls", + "xlsb", + "xlsm", + "xlsx", + "xlt", + "xltm", + "xltx", + "xm", + "xmind", + "xpi", + "xpm", + "xwd", + "xz", + "z", + "zip", + "zipx" + ]; +}); + +// ../../node_modules/binary-extensions/index.js +var require_binary_extensions2 = __commonJS((exports2, module2) => { + module2.exports = require_binary_extensions(); +}); + +// ../../node_modules/is-binary-path/index.js +var require_is_binary_path = __commonJS((exports2, module2) => { + "use strict"; + var path = require("path"); + var binaryExtensions = require_binary_extensions2(); + var extensions = new Set(binaryExtensions); + module2.exports = (filePath) => extensions.has(path.extname(filePath).slice(1).toLowerCase()); +}); + +// ../../node_modules/chokidar/lib/constants.js +var require_constants3 = __commonJS((exports2) => { + "use strict"; + var {sep} = require("path"); + var {platform} = process; + var os = require("os"); + exports2.EV_ALL = "all"; + exports2.EV_READY = "ready"; + exports2.EV_ADD = "add"; + exports2.EV_CHANGE = "change"; + exports2.EV_ADD_DIR = "addDir"; + exports2.EV_UNLINK = "unlink"; + exports2.EV_UNLINK_DIR = "unlinkDir"; + exports2.EV_RAW = "raw"; + exports2.EV_ERROR = "error"; + exports2.STR_DATA = "data"; + exports2.STR_END = "end"; + exports2.STR_CLOSE = "close"; + exports2.FSEVENT_CREATED = "created"; + exports2.FSEVENT_MODIFIED = "modified"; + exports2.FSEVENT_DELETED = "deleted"; + exports2.FSEVENT_MOVED = "moved"; + exports2.FSEVENT_CLONED = "cloned"; + exports2.FSEVENT_UNKNOWN = "unknown"; + exports2.FSEVENT_TYPE_FILE = "file"; + exports2.FSEVENT_TYPE_DIRECTORY = "directory"; + exports2.FSEVENT_TYPE_SYMLINK = "symlink"; + exports2.KEY_LISTENERS = "listeners"; + exports2.KEY_ERR = "errHandlers"; + exports2.KEY_RAW = "rawEmitters"; + exports2.HANDLER_KEYS = [exports2.KEY_LISTENERS, exports2.KEY_ERR, exports2.KEY_RAW]; + exports2.DOT_SLASH = `.${sep}`; + exports2.BACK_SLASH_RE = /\\/g; + exports2.DOUBLE_SLASH_RE = /\/\//; + exports2.SLASH_OR_BACK_SLASH_RE = /[/\\]/; + exports2.DOT_RE = /\..*\.(sw[px])$|~$|\.subl.*\.tmp/; + exports2.REPLACER_RE = /^\.[/\\]/; + exports2.SLASH = "/"; + exports2.SLASH_SLASH = "//"; + exports2.BRACE_START = "{"; + exports2.BANG = "!"; + exports2.ONE_DOT = "."; + exports2.TWO_DOTS = ".."; + exports2.STAR = "*"; + exports2.GLOBSTAR = "**"; + exports2.ROOT_GLOBSTAR = "/**/*"; + exports2.SLASH_GLOBSTAR = "/**"; + exports2.DIR_SUFFIX = "Dir"; + exports2.ANYMATCH_OPTS = {dot: true}; + exports2.STRING_TYPE = "string"; + exports2.FUNCTION_TYPE = "function"; + exports2.EMPTY_STR = ""; + exports2.EMPTY_FN = () => { + }; + exports2.IDENTITY_FN = (val) => val; + exports2.isWindows = platform === "win32"; + exports2.isMacos = platform === "darwin"; + exports2.isLinux = platform === "linux"; + exports2.isIBMi = os.type() === "OS400"; +}); + +// ../../node_modules/chokidar/lib/nodefs-handler.js +var require_nodefs_handler = __commonJS((exports2, module2) => { + "use strict"; + var fs = require("fs"); + var sysPath = require("path"); + var {promisify} = require("util"); + var isBinaryPath = require_is_binary_path(); + var { + isWindows, + isLinux, + EMPTY_FN, + EMPTY_STR, + KEY_LISTENERS, + KEY_ERR, + KEY_RAW, + HANDLER_KEYS, + EV_CHANGE, + EV_ADD, + EV_ADD_DIR, + EV_ERROR, + STR_DATA, + STR_END, + BRACE_START, + STAR + } = require_constants3(); + var THROTTLE_MODE_WATCH = "watch"; + var open = promisify(fs.open); + var stat = promisify(fs.stat); + var lstat = promisify(fs.lstat); + var close = promisify(fs.close); + var fsrealpath = promisify(fs.realpath); + var statMethods = {lstat, stat}; + var foreach = (val, fn) => { + if (val instanceof Set) { + val.forEach(fn); + } else { + fn(val); + } + }; + var addAndConvert = (main, prop, item) => { + let container = main[prop]; + if (!(container instanceof Set)) { + main[prop] = container = new Set([container]); + } + container.add(item); + }; + var clearItem = (cont) => (key) => { + const set = cont[key]; + if (set instanceof Set) { + set.clear(); + } else { + delete cont[key]; + } + }; + var delFromSet = (main, prop, item) => { + const container = main[prop]; + if (container instanceof Set) { + container.delete(item); + } else if (container === item) { + delete main[prop]; + } + }; + var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val; + var FsWatchInstances = new Map(); + function createFsWatchInstance(path, options, listener, errHandler, emitRaw) { + const handleEvent = (rawEvent, evPath) => { + listener(path); + emitRaw(rawEvent, evPath, {watchedPath: path}); + if (evPath && path !== evPath) { + fsWatchBroadcast(sysPath.resolve(path, evPath), KEY_LISTENERS, sysPath.join(path, evPath)); + } + }; + try { + return fs.watch(path, options, handleEvent); + } catch (error) { + errHandler(error); + } + } + var fsWatchBroadcast = (fullPath, type, val1, val2, val3) => { + const cont = FsWatchInstances.get(fullPath); + if (!cont) + return; + foreach(cont[type], (listener) => { + listener(val1, val2, val3); + }); + }; + var setFsWatchListener = (path, fullPath, options, handlers) => { + const {listener, errHandler, rawEmitter} = handlers; + let cont = FsWatchInstances.get(fullPath); + let watcher; + if (!options.persistent) { + watcher = createFsWatchInstance(path, options, listener, errHandler, rawEmitter); + return watcher.close.bind(watcher); + } + if (cont) { + addAndConvert(cont, KEY_LISTENERS, listener); + addAndConvert(cont, KEY_ERR, errHandler); + addAndConvert(cont, KEY_RAW, rawEmitter); + } else { + watcher = createFsWatchInstance(path, options, fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS), errHandler, fsWatchBroadcast.bind(null, fullPath, KEY_RAW)); + if (!watcher) + return; + watcher.on(EV_ERROR, async (error) => { + const broadcastErr = fsWatchBroadcast.bind(null, fullPath, KEY_ERR); + cont.watcherUnusable = true; + if (isWindows && error.code === "EPERM") { + try { + const fd = await open(path, "r"); + await close(fd); + broadcastErr(error); + } catch (err) { + } + } else { + broadcastErr(error); + } + }); + cont = { + listeners: listener, + errHandlers: errHandler, + rawEmitters: rawEmitter, + watcher + }; + FsWatchInstances.set(fullPath, cont); + } + return () => { + delFromSet(cont, KEY_LISTENERS, listener); + delFromSet(cont, KEY_ERR, errHandler); + delFromSet(cont, KEY_RAW, rawEmitter); + if (isEmptySet(cont.listeners)) { + cont.watcher.close(); + FsWatchInstances.delete(fullPath); + HANDLER_KEYS.forEach(clearItem(cont)); + cont.watcher = void 0; + Object.freeze(cont); + } + }; + }; + var FsWatchFileInstances = new Map(); + var setFsWatchFileListener = (path, fullPath, options, handlers) => { + const {listener, rawEmitter} = handlers; + let cont = FsWatchFileInstances.get(fullPath); + let listeners = new Set(); + let rawEmitters = new Set(); + const copts = cont && cont.options; + if (copts && (copts.persistent < options.persistent || copts.interval > options.interval)) { + listeners = cont.listeners; + rawEmitters = cont.rawEmitters; + fs.unwatchFile(fullPath); + cont = void 0; + } + if (cont) { + addAndConvert(cont, KEY_LISTENERS, listener); + addAndConvert(cont, KEY_RAW, rawEmitter); + } else { + cont = { + listeners: listener, + rawEmitters: rawEmitter, + options, + watcher: fs.watchFile(fullPath, options, (curr, prev) => { + foreach(cont.rawEmitters, (rawEmitter2) => { + rawEmitter2(EV_CHANGE, fullPath, {curr, prev}); + }); + const currmtime = curr.mtimeMs; + if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) { + foreach(cont.listeners, (listener2) => listener2(path, curr)); + } + }) + }; + FsWatchFileInstances.set(fullPath, cont); + } + return () => { + delFromSet(cont, KEY_LISTENERS, listener); + delFromSet(cont, KEY_RAW, rawEmitter); + if (isEmptySet(cont.listeners)) { + FsWatchFileInstances.delete(fullPath); + fs.unwatchFile(fullPath); + cont.options = cont.watcher = void 0; + Object.freeze(cont); + } + }; + }; + var NodeFsHandler = class { + constructor(fsW) { + this.fsw = fsW; + this._boundHandleError = (error) => fsW._handleError(error); + } + _watchWithNodeFs(path, listener) { + const opts = this.fsw.options; + const directory = sysPath.dirname(path); + const basename = sysPath.basename(path); + const parent = this.fsw._getWatchedDir(directory); + parent.add(basename); + const absolutePath = sysPath.resolve(path); + const options = {persistent: opts.persistent}; + if (!listener) + listener = EMPTY_FN; + let closer; + if (opts.usePolling) { + options.interval = opts.enableBinaryInterval && isBinaryPath(basename) ? opts.binaryInterval : opts.interval; + closer = setFsWatchFileListener(path, absolutePath, options, { + listener, + rawEmitter: this.fsw._emitRaw + }); + } else { + closer = setFsWatchListener(path, absolutePath, options, { + listener, + errHandler: this._boundHandleError, + rawEmitter: this.fsw._emitRaw + }); + } + return closer; + } + _handleFile(file, stats, initialAdd) { + if (this.fsw.closed) { + return; + } + const dirname = sysPath.dirname(file); + const basename = sysPath.basename(file); + const parent = this.fsw._getWatchedDir(dirname); + let prevStats = stats; + if (parent.has(basename)) + return; + const listener = async (path, newStats) => { + if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5)) + return; + if (!newStats || newStats.mtimeMs === 0) { + try { + const newStats2 = await stat(file); + if (this.fsw.closed) + return; + const at = newStats2.atimeMs; + const mt = newStats2.mtimeMs; + if (!at || at <= mt || mt !== prevStats.mtimeMs) { + this.fsw._emit(EV_CHANGE, file, newStats2); + } + if (isLinux && prevStats.ino !== newStats2.ino) { + this.fsw._closeFile(path); + prevStats = newStats2; + this.fsw._addPathCloser(path, this._watchWithNodeFs(file, listener)); + } else { + prevStats = newStats2; + } + } catch (error) { + this.fsw._remove(dirname, basename); + } + } else if (parent.has(basename)) { + const at = newStats.atimeMs; + const mt = newStats.mtimeMs; + if (!at || at <= mt || mt !== prevStats.mtimeMs) { + this.fsw._emit(EV_CHANGE, file, newStats); + } + prevStats = newStats; + } + }; + const closer = this._watchWithNodeFs(file, listener); + if (!(initialAdd && this.fsw.options.ignoreInitial) && this.fsw._isntIgnored(file)) { + if (!this.fsw._throttle(EV_ADD, file, 0)) + return; + this.fsw._emit(EV_ADD, file, stats); + } + return closer; + } + async _handleSymlink(entry, directory, path, item) { + if (this.fsw.closed) { + return; + } + const full = entry.fullPath; + const dir = this.fsw._getWatchedDir(directory); + if (!this.fsw.options.followSymlinks) { + this.fsw._incrReadyCount(); + const linkPath = await fsrealpath(path); + if (this.fsw.closed) + return; + if (dir.has(item)) { + if (this.fsw._symlinkPaths.get(full) !== linkPath) { + this.fsw._symlinkPaths.set(full, linkPath); + this.fsw._emit(EV_CHANGE, path, entry.stats); + } + } else { + dir.add(item); + this.fsw._symlinkPaths.set(full, linkPath); + this.fsw._emit(EV_ADD, path, entry.stats); + } + this.fsw._emitReady(); + return true; + } + if (this.fsw._symlinkPaths.has(full)) { + return true; + } + this.fsw._symlinkPaths.set(full, true); + } + _handleRead(directory, initialAdd, wh, target, dir, depth, throttler) { + directory = sysPath.join(directory, EMPTY_STR); + if (!wh.hasGlob) { + throttler = this.fsw._throttle("readdir", directory, 1e3); + if (!throttler) + return; + } + const previous = this.fsw._getWatchedDir(wh.path); + const current = new Set(); + let stream = this.fsw._readdirp(directory, { + fileFilter: (entry) => wh.filterPath(entry), + directoryFilter: (entry) => wh.filterDir(entry), + depth: 0 + }).on(STR_DATA, async (entry) => { + if (this.fsw.closed) { + stream = void 0; + return; + } + const item = entry.path; + let path = sysPath.join(directory, item); + current.add(item); + if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path, item)) { + return; + } + if (this.fsw.closed) { + stream = void 0; + return; + } + if (item === target || !target && !previous.has(item)) { + this.fsw._incrReadyCount(); + path = sysPath.join(dir, sysPath.relative(dir, path)); + this._addToNodeFs(path, initialAdd, wh, depth + 1); + } + }).on(EV_ERROR, this._boundHandleError); + return new Promise((resolve) => stream.once(STR_END, () => { + if (this.fsw.closed) { + stream = void 0; + return; + } + const wasThrottled = throttler ? throttler.clear() : false; + resolve(); + previous.getChildren().filter((item) => { + return item !== directory && !current.has(item) && (!wh.hasGlob || wh.filterPath({ + fullPath: sysPath.resolve(directory, item) + })); + }).forEach((item) => { + this.fsw._remove(directory, item); + }); + stream = void 0; + if (wasThrottled) + this._handleRead(directory, false, wh, target, dir, depth, throttler); + })); + } + async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath) { + const parentDir = this.fsw._getWatchedDir(sysPath.dirname(dir)); + const tracked = parentDir.has(sysPath.basename(dir)); + if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) { + if (!wh.hasGlob || wh.globFilter(dir)) + this.fsw._emit(EV_ADD_DIR, dir, stats); + } + parentDir.add(sysPath.basename(dir)); + this.fsw._getWatchedDir(dir); + let throttler; + let closer; + const oDepth = this.fsw.options.depth; + if ((oDepth == null || depth <= oDepth) && !this.fsw._symlinkPaths.has(realpath)) { + if (!target) { + await this._handleRead(dir, initialAdd, wh, target, dir, depth, throttler); + if (this.fsw.closed) + return; + } + closer = this._watchWithNodeFs(dir, (dirPath, stats2) => { + if (stats2 && stats2.mtimeMs === 0) + return; + this._handleRead(dirPath, false, wh, target, dir, depth, throttler); + }); + } + return closer; + } + async _addToNodeFs(path, initialAdd, priorWh, depth, target) { + const ready = this.fsw._emitReady; + if (this.fsw._isIgnored(path) || this.fsw.closed) { + ready(); + return false; + } + const wh = this.fsw._getWatchHelpers(path, depth); + if (!wh.hasGlob && priorWh) { + wh.hasGlob = priorWh.hasGlob; + wh.globFilter = priorWh.globFilter; + wh.filterPath = (entry) => priorWh.filterPath(entry); + wh.filterDir = (entry) => priorWh.filterDir(entry); + } + try { + const stats = await statMethods[wh.statMethod](wh.watchPath); + if (this.fsw.closed) + return; + if (this.fsw._isIgnored(wh.watchPath, stats)) { + ready(); + return false; + } + const follow = this.fsw.options.followSymlinks && !path.includes(STAR) && !path.includes(BRACE_START); + let closer; + if (stats.isDirectory()) { + const absPath = sysPath.resolve(path); + const targetPath = follow ? await fsrealpath(path) : path; + if (this.fsw.closed) + return; + closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath); + if (this.fsw.closed) + return; + if (absPath !== targetPath && targetPath !== void 0) { + this.fsw._symlinkPaths.set(absPath, targetPath); + } + } else if (stats.isSymbolicLink()) { + const targetPath = follow ? await fsrealpath(path) : path; + if (this.fsw.closed) + return; + const parent = sysPath.dirname(wh.watchPath); + this.fsw._getWatchedDir(parent).add(wh.watchPath); + this.fsw._emit(EV_ADD, wh.watchPath, stats); + closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath); + if (this.fsw.closed) + return; + if (targetPath !== void 0) { + this.fsw._symlinkPaths.set(sysPath.resolve(path), targetPath); + } + } else { + closer = this._handleFile(wh.watchPath, stats, initialAdd); + } + ready(); + this.fsw._addPathCloser(path, closer); + return false; + } catch (error) { + if (this.fsw._handleError(error)) { + ready(); + return path; + } + } + } + }; + module2.exports = NodeFsHandler; +}); + +// ../../node_modules/chokidar/lib/fsevents-handler.js +var require_fsevents_handler = __commonJS((exports2, module2) => { + "use strict"; + var fs = require("fs"); + var sysPath = require("path"); + var {promisify} = require("util"); + var fsevents; + try { + fsevents = require("fsevents"); + } catch (error) { + if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) + console.error(error); + } + if (fsevents) { + const mtch = process.version.match(/v(\d+)\.(\d+)/); + if (mtch && mtch[1] && mtch[2]) { + const maj = Number.parseInt(mtch[1], 10); + const min = Number.parseInt(mtch[2], 10); + if (maj === 8 && min < 16) { + fsevents = void 0; + } + } + } + var { + EV_ADD, + EV_CHANGE, + EV_ADD_DIR, + EV_UNLINK, + EV_ERROR, + STR_DATA, + STR_END, + FSEVENT_CREATED, + FSEVENT_MODIFIED, + FSEVENT_DELETED, + FSEVENT_MOVED, + FSEVENT_UNKNOWN, + FSEVENT_TYPE_FILE, + FSEVENT_TYPE_DIRECTORY, + FSEVENT_TYPE_SYMLINK, + ROOT_GLOBSTAR, + DIR_SUFFIX, + DOT_SLASH, + FUNCTION_TYPE, + EMPTY_FN, + IDENTITY_FN + } = require_constants3(); + var Depth = (value) => isNaN(value) ? {} : {depth: value}; + var stat = promisify(fs.stat); + var lstat = promisify(fs.lstat); + var realpath = promisify(fs.realpath); + var statMethods = {stat, lstat}; + var FSEventsWatchers = new Map(); + var consolidateThreshhold = 10; + var wrongEventFlags = new Set([ + 69888, + 70400, + 71424, + 72704, + 73472, + 131328, + 131840, + 262912 + ]); + var createFSEventsInstance = (path, callback) => { + const stop = fsevents.watch(path, callback); + return {stop}; + }; + function setFSEventsListener(path, realPath, listener, rawEmitter) { + let watchPath = sysPath.extname(realPath) ? sysPath.dirname(realPath) : realPath; + const parentPath = sysPath.dirname(watchPath); + let cont = FSEventsWatchers.get(watchPath); + if (couldConsolidate(parentPath)) { + watchPath = parentPath; + } + const resolvedPath = sysPath.resolve(path); + const hasSymlink = resolvedPath !== realPath; + const filteredListener = (fullPath, flags, info) => { + if (hasSymlink) + fullPath = fullPath.replace(realPath, resolvedPath); + if (fullPath === resolvedPath || !fullPath.indexOf(resolvedPath + sysPath.sep)) + listener(fullPath, flags, info); + }; + let watchedParent = false; + for (const watchedPath of FSEventsWatchers.keys()) { + if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) { + watchPath = watchedPath; + cont = FSEventsWatchers.get(watchPath); + watchedParent = true; + break; + } + } + if (cont || watchedParent) { + cont.listeners.add(filteredListener); + } else { + cont = { + listeners: new Set([filteredListener]), + rawEmitter, + watcher: createFSEventsInstance(watchPath, (fullPath, flags) => { + if (!cont.listeners.size) + return; + const info = fsevents.getInfo(fullPath, flags); + cont.listeners.forEach((list) => { + list(fullPath, flags, info); + }); + cont.rawEmitter(info.event, fullPath, info); + }) + }; + FSEventsWatchers.set(watchPath, cont); + } + return () => { + const lst = cont.listeners; + lst.delete(filteredListener); + if (!lst.size) { + FSEventsWatchers.delete(watchPath); + if (cont.watcher) + return cont.watcher.stop().then(() => { + cont.rawEmitter = cont.watcher = void 0; + Object.freeze(cont); + }); + } + }; + } + var couldConsolidate = (path) => { + let count = 0; + for (const watchPath of FSEventsWatchers.keys()) { + if (watchPath.indexOf(path) === 0) { + count++; + if (count >= consolidateThreshhold) { + return true; + } + } + } + return false; + }; + var canUse = () => fsevents && FSEventsWatchers.size < 128; + var calcDepth = (path, root) => { + let i = 0; + while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) + i++; + return i; + }; + var sameTypes = (info, stats) => info.type === FSEVENT_TYPE_DIRECTORY && stats.isDirectory() || info.type === FSEVENT_TYPE_SYMLINK && stats.isSymbolicLink() || info.type === FSEVENT_TYPE_FILE && stats.isFile(); + var FsEventsHandler = class { + constructor(fsw) { + this.fsw = fsw; + } + checkIgnored(path, stats) { + const ipaths = this.fsw._ignoredPaths; + if (this.fsw._isIgnored(path, stats)) { + ipaths.add(path); + if (stats && stats.isDirectory()) { + ipaths.add(path + ROOT_GLOBSTAR); + } + return true; + } + ipaths.delete(path); + ipaths.delete(path + ROOT_GLOBSTAR); + } + addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts) { + const event = watchedDir.has(item) ? EV_CHANGE : EV_ADD; + this.handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts); + } + async checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts) { + try { + const stats = await stat(path); + if (this.fsw.closed) + return; + if (sameTypes(info, stats)) { + this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts); + } else { + this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts); + } + } catch (error) { + if (error.code === "EACCES") { + this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts); + } else { + this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts); + } + } + } + handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts) { + if (this.fsw.closed || this.checkIgnored(path)) + return; + if (event === EV_UNLINK) { + const isDirectory = info.type === FSEVENT_TYPE_DIRECTORY; + if (isDirectory || watchedDir.has(item)) { + this.fsw._remove(parent, item, isDirectory); + } + } else { + if (event === EV_ADD) { + if (info.type === FSEVENT_TYPE_DIRECTORY) + this.fsw._getWatchedDir(path); + if (info.type === FSEVENT_TYPE_SYMLINK && opts.followSymlinks) { + const curDepth = opts.depth === void 0 ? void 0 : calcDepth(fullPath, realPath) + 1; + return this._addToFsEvents(path, false, true, curDepth); + } + this.fsw._getWatchedDir(parent).add(item); + } + const eventName = info.type === FSEVENT_TYPE_DIRECTORY ? event + DIR_SUFFIX : event; + this.fsw._emit(eventName, path); + if (eventName === EV_ADD_DIR) + this._addToFsEvents(path, false, true); + } + } + _watchWithFsEvents(watchPath, realPath, transform, globFilter) { + if (this.fsw.closed || this.fsw._isIgnored(watchPath)) + return; + const opts = this.fsw.options; + const watchCallback = async (fullPath, flags, info) => { + if (this.fsw.closed) + return; + if (opts.depth !== void 0 && calcDepth(fullPath, realPath) > opts.depth) + return; + const path = transform(sysPath.join(watchPath, sysPath.relative(watchPath, fullPath))); + if (globFilter && !globFilter(path)) + return; + const parent = sysPath.dirname(path); + const item = sysPath.basename(path); + const watchedDir = this.fsw._getWatchedDir(info.type === FSEVENT_TYPE_DIRECTORY ? path : parent); + if (wrongEventFlags.has(flags) || info.event === FSEVENT_UNKNOWN) { + if (typeof opts.ignored === FUNCTION_TYPE) { + let stats; + try { + stats = await stat(path); + } catch (error) { + } + if (this.fsw.closed) + return; + if (this.checkIgnored(path, stats)) + return; + if (sameTypes(info, stats)) { + this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts); + } else { + this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts); + } + } else { + this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts); + } + } else { + switch (info.event) { + case FSEVENT_CREATED: + case FSEVENT_MODIFIED: + return this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts); + case FSEVENT_DELETED: + case FSEVENT_MOVED: + return this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts); + } + } + }; + const closer = setFSEventsListener(watchPath, realPath, watchCallback, this.fsw._emitRaw); + this.fsw._emitReady(); + return closer; + } + async _handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) { + if (this.fsw.closed || this.fsw._symlinkPaths.has(fullPath)) + return; + this.fsw._symlinkPaths.set(fullPath, true); + this.fsw._incrReadyCount(); + try { + const linkTarget = await realpath(linkPath); + if (this.fsw.closed) + return; + if (this.fsw._isIgnored(linkTarget)) { + return this.fsw._emitReady(); + } + this.fsw._incrReadyCount(); + this._addToFsEvents(linkTarget || linkPath, (path) => { + let aliasedPath = linkPath; + if (linkTarget && linkTarget !== DOT_SLASH) { + aliasedPath = path.replace(linkTarget, linkPath); + } else if (path !== DOT_SLASH) { + aliasedPath = sysPath.join(linkPath, path); + } + return transform(aliasedPath); + }, false, curDepth); + } catch (error) { + if (this.fsw._handleError(error)) { + return this.fsw._emitReady(); + } + } + } + emitAdd(newPath, stats, processPath, opts, forceAdd) { + const pp = processPath(newPath); + const isDir = stats.isDirectory(); + const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp)); + const base = sysPath.basename(pp); + if (isDir) + this.fsw._getWatchedDir(pp); + if (dirObj.has(base)) + return; + dirObj.add(base); + if (!opts.ignoreInitial || forceAdd === true) { + this.fsw._emit(isDir ? EV_ADD_DIR : EV_ADD, pp, stats); + } + } + initWatch(realPath, path, wh, processPath) { + if (this.fsw.closed) + return; + const closer = this._watchWithFsEvents(wh.watchPath, sysPath.resolve(realPath || wh.watchPath), processPath, wh.globFilter); + this.fsw._addPathCloser(path, closer); + } + async _addToFsEvents(path, transform, forceAdd, priorDepth) { + if (this.fsw.closed) { + return; + } + const opts = this.fsw.options; + const processPath = typeof transform === FUNCTION_TYPE ? transform : IDENTITY_FN; + const wh = this.fsw._getWatchHelpers(path); + try { + const stats = await statMethods[wh.statMethod](wh.watchPath); + if (this.fsw.closed) + return; + if (this.fsw._isIgnored(wh.watchPath, stats)) { + throw null; + } + if (stats.isDirectory()) { + if (!wh.globFilter) + this.emitAdd(processPath(path), stats, processPath, opts, forceAdd); + if (priorDepth && priorDepth > opts.depth) + return; + this.fsw._readdirp(wh.watchPath, { + fileFilter: (entry) => wh.filterPath(entry), + directoryFilter: (entry) => wh.filterDir(entry), + ...Depth(opts.depth - (priorDepth || 0)) + }).on(STR_DATA, (entry) => { + if (this.fsw.closed) { + return; + } + if (entry.stats.isDirectory() && !wh.filterPath(entry)) + return; + const joinedPath = sysPath.join(wh.watchPath, entry.path); + const {fullPath} = entry; + if (wh.followSymlinks && entry.stats.isSymbolicLink()) { + const curDepth = opts.depth === void 0 ? void 0 : calcDepth(joinedPath, sysPath.resolve(wh.watchPath)) + 1; + this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth); + } else { + this.emitAdd(joinedPath, entry.stats, processPath, opts, forceAdd); + } + }).on(EV_ERROR, EMPTY_FN).on(STR_END, () => { + this.fsw._emitReady(); + }); + } else { + this.emitAdd(wh.watchPath, stats, processPath, opts, forceAdd); + this.fsw._emitReady(); + } + } catch (error) { + if (!error || this.fsw._handleError(error)) { + this.fsw._emitReady(); + this.fsw._emitReady(); + } + } + if (opts.persistent && forceAdd !== true) { + if (typeof transform === FUNCTION_TYPE) { + this.initWatch(void 0, path, wh, processPath); + } else { + let realPath; + try { + realPath = await realpath(wh.watchPath); + } catch (e2) { + } + this.initWatch(realPath, path, wh, processPath); + } + } + } + }; + module2.exports = FsEventsHandler; + module2.exports.canUse = canUse; +}); + +// ../../node_modules/chokidar/index.js +var require_chokidar = __commonJS((exports2) => { + "use strict"; + var {EventEmitter} = require("events"); + var fs = require("fs"); + var sysPath = require("path"); + var {promisify} = require("util"); + var readdirp = require_readdirp(); + var anymatch = require_anymatch().default; + var globParent = require_glob_parent(); + var isGlob = require_is_glob(); + var braces = require_braces(); + var normalizePath = require_normalize_path(); + var NodeFsHandler = require_nodefs_handler(); + var FsEventsHandler = require_fsevents_handler(); + var { + EV_ALL, + EV_READY, + EV_ADD, + EV_CHANGE, + EV_UNLINK, + EV_ADD_DIR, + EV_UNLINK_DIR, + EV_RAW, + EV_ERROR, + STR_CLOSE, + STR_END, + BACK_SLASH_RE, + DOUBLE_SLASH_RE, + SLASH_OR_BACK_SLASH_RE, + DOT_RE, + REPLACER_RE, + SLASH, + SLASH_SLASH, + BRACE_START, + BANG, + ONE_DOT, + TWO_DOTS, + GLOBSTAR, + SLASH_GLOBSTAR, + ANYMATCH_OPTS, + STRING_TYPE, + FUNCTION_TYPE, + EMPTY_STR, + EMPTY_FN, + isWindows, + isMacos, + isIBMi + } = require_constants3(); + var stat = promisify(fs.stat); + var readdir = promisify(fs.readdir); + var arrify = (value = []) => Array.isArray(value) ? value : [value]; + var flatten = (list, result = []) => { + list.forEach((item) => { + if (Array.isArray(item)) { + flatten(item, result); + } else { + result.push(item); + } + }); + return result; + }; + var unifyPaths = (paths_) => { + const paths = flatten(arrify(paths_)); + if (!paths.every((p) => typeof p === STRING_TYPE)) { + throw new TypeError(`Non-string provided as watch path: ${paths}`); + } + return paths.map(normalizePathToUnix); + }; + var toUnix = (string) => { + let str = string.replace(BACK_SLASH_RE, SLASH); + let prepend = false; + if (str.startsWith(SLASH_SLASH)) { + prepend = true; + } + while (str.match(DOUBLE_SLASH_RE)) { + str = str.replace(DOUBLE_SLASH_RE, SLASH); + } + if (prepend) { + str = SLASH + str; + } + return str; + }; + var normalizePathToUnix = (path) => toUnix(sysPath.normalize(toUnix(path))); + var normalizeIgnored = (cwd = EMPTY_STR) => (path) => { + if (typeof path !== STRING_TYPE) + return path; + return normalizePathToUnix(sysPath.isAbsolute(path) ? path : sysPath.join(cwd, path)); + }; + var getAbsolutePath = (path, cwd) => { + if (sysPath.isAbsolute(path)) { + return path; + } + if (path.startsWith(BANG)) { + return BANG + sysPath.join(cwd, path.slice(1)); + } + return sysPath.join(cwd, path); + }; + var undef = (opts, key) => opts[key] === void 0; + var DirEntry = class { + constructor(dir, removeWatcher) { + this.path = dir; + this._removeWatcher = removeWatcher; + this.items = new Set(); + } + add(item) { + const {items} = this; + if (!items) + return; + if (item !== ONE_DOT && item !== TWO_DOTS) + items.add(item); + } + async remove(item) { + const {items} = this; + if (!items) + return; + items.delete(item); + if (items.size > 0) + return; + const dir = this.path; + try { + await readdir(dir); + } catch (err) { + if (this._removeWatcher) { + this._removeWatcher(sysPath.dirname(dir), sysPath.basename(dir)); + } + } + } + has(item) { + const {items} = this; + if (!items) + return; + return items.has(item); + } + getChildren() { + const {items} = this; + if (!items) + return; + return [...items.values()]; + } + dispose() { + this.items.clear(); + delete this.path; + delete this._removeWatcher; + delete this.items; + Object.freeze(this); + } + }; + var STAT_METHOD_F = "stat"; + var STAT_METHOD_L = "lstat"; + var WatchHelper = class { + constructor(path, watchPath, follow, fsw) { + this.fsw = fsw; + this.path = path = path.replace(REPLACER_RE, EMPTY_STR); + this.watchPath = watchPath; + this.fullWatchPath = sysPath.resolve(watchPath); + this.hasGlob = watchPath !== path; + if (path === EMPTY_STR) + this.hasGlob = false; + this.globSymlink = this.hasGlob && follow ? void 0 : false; + this.globFilter = this.hasGlob ? anymatch(path, void 0, ANYMATCH_OPTS) : false; + this.dirParts = this.getDirParts(path); + this.dirParts.forEach((parts) => { + if (parts.length > 1) + parts.pop(); + }); + this.followSymlinks = follow; + this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L; + } + checkGlobSymlink(entry) { + if (this.globSymlink === void 0) { + this.globSymlink = entry.fullParentDir === this.fullWatchPath ? false : {realPath: entry.fullParentDir, linkPath: this.fullWatchPath}; + } + if (this.globSymlink) { + return entry.fullPath.replace(this.globSymlink.realPath, this.globSymlink.linkPath); + } + return entry.fullPath; + } + entryPath(entry) { + return sysPath.join(this.watchPath, sysPath.relative(this.watchPath, this.checkGlobSymlink(entry))); + } + filterPath(entry) { + const {stats} = entry; + if (stats && stats.isSymbolicLink()) + return this.filterDir(entry); + const resolvedPath = this.entryPath(entry); + const matchesGlob = this.hasGlob && typeof this.globFilter === FUNCTION_TYPE ? this.globFilter(resolvedPath) : true; + return matchesGlob && this.fsw._isntIgnored(resolvedPath, stats) && this.fsw._hasReadPermissions(stats); + } + getDirParts(path) { + if (!this.hasGlob) + return []; + const parts = []; + const expandedPath = path.includes(BRACE_START) ? braces.expand(path) : [path]; + expandedPath.forEach((path2) => { + parts.push(sysPath.relative(this.watchPath, path2).split(SLASH_OR_BACK_SLASH_RE)); + }); + return parts; + } + filterDir(entry) { + if (this.hasGlob) { + const entryParts = this.getDirParts(this.checkGlobSymlink(entry)); + let globstar = false; + this.unmatchedGlob = !this.dirParts.some((parts) => { + return parts.every((part, i) => { + if (part === GLOBSTAR) + globstar = true; + return globstar || !entryParts[0][i] || anymatch(part, entryParts[0][i], ANYMATCH_OPTS); + }); + }); + } + return !this.unmatchedGlob && this.fsw._isntIgnored(this.entryPath(entry), entry.stats); + } + }; + var FSWatcher = class extends EventEmitter { + constructor(_opts) { + super(); + const opts = {}; + if (_opts) + Object.assign(opts, _opts); + this._watched = new Map(); + this._closers = new Map(); + this._ignoredPaths = new Set(); + this._throttled = new Map(); + this._symlinkPaths = new Map(); + this._streams = new Set(); + this.closed = false; + if (undef(opts, "persistent")) + opts.persistent = true; + if (undef(opts, "ignoreInitial")) + opts.ignoreInitial = false; + if (undef(opts, "ignorePermissionErrors")) + opts.ignorePermissionErrors = false; + if (undef(opts, "interval")) + opts.interval = 100; + if (undef(opts, "binaryInterval")) + opts.binaryInterval = 300; + if (undef(opts, "disableGlobbing")) + opts.disableGlobbing = false; + opts.enableBinaryInterval = opts.binaryInterval !== opts.interval; + if (undef(opts, "useFsEvents")) + opts.useFsEvents = !opts.usePolling; + const canUseFsEvents = FsEventsHandler.canUse(); + if (!canUseFsEvents) + opts.useFsEvents = false; + if (undef(opts, "usePolling") && !opts.useFsEvents) { + opts.usePolling = isMacos; + } + if (isIBMi) { + opts.usePolling = true; + } + const envPoll = process.env.CHOKIDAR_USEPOLLING; + if (envPoll !== void 0) { + const envLower = envPoll.toLowerCase(); + if (envLower === "false" || envLower === "0") { + opts.usePolling = false; + } else if (envLower === "true" || envLower === "1") { + opts.usePolling = true; + } else { + opts.usePolling = !!envLower; + } + } + const envInterval = process.env.CHOKIDAR_INTERVAL; + if (envInterval) { + opts.interval = Number.parseInt(envInterval, 10); + } + if (undef(opts, "atomic")) + opts.atomic = !opts.usePolling && !opts.useFsEvents; + if (opts.atomic) + this._pendingUnlinks = new Map(); + if (undef(opts, "followSymlinks")) + opts.followSymlinks = true; + if (undef(opts, "awaitWriteFinish")) + opts.awaitWriteFinish = false; + if (opts.awaitWriteFinish === true) + opts.awaitWriteFinish = {}; + const awf = opts.awaitWriteFinish; + if (awf) { + if (!awf.stabilityThreshold) + awf.stabilityThreshold = 2e3; + if (!awf.pollInterval) + awf.pollInterval = 100; + this._pendingWrites = new Map(); + } + if (opts.ignored) + opts.ignored = arrify(opts.ignored); + let readyCalls = 0; + this._emitReady = () => { + readyCalls++; + if (readyCalls >= this._readyCount) { + this._emitReady = EMPTY_FN; + this._readyEmitted = true; + process.nextTick(() => this.emit(EV_READY)); + } + }; + this._emitRaw = (...args) => this.emit(EV_RAW, ...args); + this._readyEmitted = false; + this.options = opts; + if (opts.useFsEvents) { + this._fsEventsHandler = new FsEventsHandler(this); + } else { + this._nodeFsHandler = new NodeFsHandler(this); + } + Object.freeze(opts); + } + add(paths_, _origAdd, _internal) { + const {cwd, disableGlobbing} = this.options; + this.closed = false; + let paths = unifyPaths(paths_); + if (cwd) { + paths = paths.map((path) => { + const absPath = getAbsolutePath(path, cwd); + if (disableGlobbing || !isGlob(path)) { + return absPath; + } + return normalizePath(absPath); + }); + } + paths = paths.filter((path) => { + if (path.startsWith(BANG)) { + this._ignoredPaths.add(path.slice(1)); + return false; + } + this._ignoredPaths.delete(path); + this._ignoredPaths.delete(path + SLASH_GLOBSTAR); + this._userIgnored = void 0; + return true; + }); + if (this.options.useFsEvents && this._fsEventsHandler) { + if (!this._readyCount) + this._readyCount = paths.length; + if (this.options.persistent) + this._readyCount *= 2; + paths.forEach((path) => this._fsEventsHandler._addToFsEvents(path)); + } else { + if (!this._readyCount) + this._readyCount = 0; + this._readyCount += paths.length; + Promise.all(paths.map(async (path) => { + const res = await this._nodeFsHandler._addToNodeFs(path, !_internal, 0, 0, _origAdd); + if (res) + this._emitReady(); + return res; + })).then((results) => { + if (this.closed) + return; + results.filter((item) => item).forEach((item) => { + this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item)); + }); + }); + } + return this; + } + unwatch(paths_) { + if (this.closed) + return this; + const paths = unifyPaths(paths_); + const {cwd} = this.options; + paths.forEach((path) => { + if (!sysPath.isAbsolute(path) && !this._closers.has(path)) { + if (cwd) + path = sysPath.join(cwd, path); + path = sysPath.resolve(path); + } + this._closePath(path); + this._ignoredPaths.add(path); + if (this._watched.has(path)) { + this._ignoredPaths.add(path + SLASH_GLOBSTAR); + } + this._userIgnored = void 0; + }); + return this; + } + close() { + if (this.closed) + return this._closePromise; + this.closed = true; + this.removeAllListeners(); + const closers = []; + this._closers.forEach((closerList) => closerList.forEach((closer) => { + const promise = closer(); + if (promise instanceof Promise) + closers.push(promise); + })); + this._streams.forEach((stream) => stream.destroy()); + this._userIgnored = void 0; + this._readyCount = 0; + this._readyEmitted = false; + this._watched.forEach((dirent) => dirent.dispose()); + ["closers", "watched", "streams", "symlinkPaths", "throttled"].forEach((key) => { + this[`_${key}`].clear(); + }); + this._closePromise = closers.length ? Promise.all(closers).then(() => void 0) : Promise.resolve(); + return this._closePromise; + } + getWatched() { + const watchList = {}; + this._watched.forEach((entry, dir) => { + const key = this.options.cwd ? sysPath.relative(this.options.cwd, dir) : dir; + watchList[key || ONE_DOT] = entry.getChildren().sort(); + }); + return watchList; + } + emitWithAll(event, args) { + this.emit(...args); + if (event !== EV_ERROR) + this.emit(EV_ALL, ...args); + } + async _emit(event, path, val1, val2, val3) { + if (this.closed) + return; + const opts = this.options; + if (isWindows) + path = sysPath.normalize(path); + if (opts.cwd) + path = sysPath.relative(opts.cwd, path); + const args = [event, path]; + if (val3 !== void 0) + args.push(val1, val2, val3); + else if (val2 !== void 0) + args.push(val1, val2); + else if (val1 !== void 0) + args.push(val1); + const awf = opts.awaitWriteFinish; + let pw; + if (awf && (pw = this._pendingWrites.get(path))) { + pw.lastChange = new Date(); + return this; + } + if (opts.atomic) { + if (event === EV_UNLINK) { + this._pendingUnlinks.set(path, args); + setTimeout(() => { + this._pendingUnlinks.forEach((entry, path2) => { + this.emit(...entry); + this.emit(EV_ALL, ...entry); + this._pendingUnlinks.delete(path2); + }); + }, typeof opts.atomic === "number" ? opts.atomic : 100); + return this; + } + if (event === EV_ADD && this._pendingUnlinks.has(path)) { + event = args[0] = EV_CHANGE; + this._pendingUnlinks.delete(path); + } + } + if (awf && (event === EV_ADD || event === EV_CHANGE) && this._readyEmitted) { + const awfEmit = (err, stats) => { + if (err) { + event = args[0] = EV_ERROR; + args[1] = err; + this.emitWithAll(event, args); + } else if (stats) { + if (args.length > 2) { + args[2] = stats; + } else { + args.push(stats); + } + this.emitWithAll(event, args); + } + }; + this._awaitWriteFinish(path, awf.stabilityThreshold, event, awfEmit); + return this; + } + if (event === EV_CHANGE) { + const isThrottled = !this._throttle(EV_CHANGE, path, 50); + if (isThrottled) + return this; + } + if (opts.alwaysStat && val1 === void 0 && (event === EV_ADD || event === EV_ADD_DIR || event === EV_CHANGE)) { + const fullPath = opts.cwd ? sysPath.join(opts.cwd, path) : path; + let stats; + try { + stats = await stat(fullPath); + } catch (err) { + } + if (!stats || this.closed) + return; + args.push(stats); + } + this.emitWithAll(event, args); + return this; + } + _handleError(error) { + const code = error && error.code; + if (error && code !== "ENOENT" && code !== "ENOTDIR" && (!this.options.ignorePermissionErrors || code !== "EPERM" && code !== "EACCES")) { + this.emit(EV_ERROR, error); + } + return error || this.closed; + } + _throttle(actionType, path, timeout) { + if (!this._throttled.has(actionType)) { + this._throttled.set(actionType, new Map()); + } + const action = this._throttled.get(actionType); + const actionPath = action.get(path); + if (actionPath) { + actionPath.count++; + return false; + } + let timeoutObject; + const clear = () => { + const item = action.get(path); + const count = item ? item.count : 0; + action.delete(path); + clearTimeout(timeoutObject); + if (item) + clearTimeout(item.timeoutObject); + return count; + }; + timeoutObject = setTimeout(clear, timeout); + const thr = {timeoutObject, clear, count: 0}; + action.set(path, thr); + return thr; + } + _incrReadyCount() { + return this._readyCount++; + } + _awaitWriteFinish(path, threshold, event, awfEmit) { + let timeoutHandler; + let fullPath = path; + if (this.options.cwd && !sysPath.isAbsolute(path)) { + fullPath = sysPath.join(this.options.cwd, path); + } + const now = new Date(); + const awaitWriteFinish = (prevStat) => { + fs.stat(fullPath, (err, curStat) => { + if (err || !this._pendingWrites.has(path)) { + if (err && err.code !== "ENOENT") + awfEmit(err); + return; + } + const now2 = Number(new Date()); + if (prevStat && curStat.size !== prevStat.size) { + this._pendingWrites.get(path).lastChange = now2; + } + const pw = this._pendingWrites.get(path); + const df = now2 - pw.lastChange; + if (df >= threshold) { + this._pendingWrites.delete(path); + awfEmit(void 0, curStat); + } else { + timeoutHandler = setTimeout(awaitWriteFinish, this.options.awaitWriteFinish.pollInterval, curStat); + } + }); + }; + if (!this._pendingWrites.has(path)) { + this._pendingWrites.set(path, { + lastChange: now, + cancelWait: () => { + this._pendingWrites.delete(path); + clearTimeout(timeoutHandler); + return event; + } + }); + timeoutHandler = setTimeout(awaitWriteFinish, this.options.awaitWriteFinish.pollInterval); + } + } + _getGlobIgnored() { + return [...this._ignoredPaths.values()]; + } + _isIgnored(path, stats) { + if (this.options.atomic && DOT_RE.test(path)) + return true; + if (!this._userIgnored) { + const {cwd} = this.options; + const ign = this.options.ignored; + const ignored = ign && ign.map(normalizeIgnored(cwd)); + const paths = arrify(ignored).filter((path2) => typeof path2 === STRING_TYPE && !isGlob(path2)).map((path2) => path2 + SLASH_GLOBSTAR); + const list = this._getGlobIgnored().map(normalizeIgnored(cwd)).concat(ignored, paths); + this._userIgnored = anymatch(list, void 0, ANYMATCH_OPTS); + } + return this._userIgnored([path, stats]); + } + _isntIgnored(path, stat2) { + return !this._isIgnored(path, stat2); + } + _getWatchHelpers(path, depth) { + const watchPath = depth || this.options.disableGlobbing || !isGlob(path) ? path : globParent(path); + const follow = this.options.followSymlinks; + return new WatchHelper(path, watchPath, follow, this); + } + _getWatchedDir(directory) { + if (!this._boundRemove) + this._boundRemove = this._remove.bind(this); + const dir = sysPath.resolve(directory); + if (!this._watched.has(dir)) + this._watched.set(dir, new DirEntry(dir, this._boundRemove)); + return this._watched.get(dir); + } + _hasReadPermissions(stats) { + if (this.options.ignorePermissionErrors) + return true; + const md = stats && Number.parseInt(stats.mode, 10); + const st = md & 511; + const it = Number.parseInt(st.toString(8)[0], 10); + return Boolean(4 & it); + } + _remove(directory, item, isDirectory) { + const path = sysPath.join(directory, item); + const fullPath = sysPath.resolve(path); + isDirectory = isDirectory != null ? isDirectory : this._watched.has(path) || this._watched.has(fullPath); + if (!this._throttle("remove", path, 100)) + return; + if (!isDirectory && !this.options.useFsEvents && this._watched.size === 1) { + this.add(directory, item, true); + } + const wp = this._getWatchedDir(path); + const nestedDirectoryChildren = wp.getChildren(); + nestedDirectoryChildren.forEach((nested) => this._remove(path, nested)); + const parent = this._getWatchedDir(directory); + const wasTracked = parent.has(item); + parent.remove(item); + if (this._symlinkPaths.has(fullPath)) { + this._symlinkPaths.delete(fullPath); + } + let relPath = path; + if (this.options.cwd) + relPath = sysPath.relative(this.options.cwd, path); + if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) { + const event = this._pendingWrites.get(relPath).cancelWait(); + if (event === EV_ADD) + return; + } + this._watched.delete(path); + this._watched.delete(fullPath); + const eventName = isDirectory ? EV_UNLINK_DIR : EV_UNLINK; + if (wasTracked && !this._isIgnored(path)) + this._emit(eventName, path); + if (!this.options.useFsEvents) { + this._closePath(path); + } + } + _closePath(path) { + this._closeFile(path); + const dir = sysPath.dirname(path); + this._getWatchedDir(dir).remove(sysPath.basename(path)); + } + _closeFile(path) { + const closers = this._closers.get(path); + if (!closers) + return; + closers.forEach((closer) => closer()); + this._closers.delete(path); + } + _addPathCloser(path, closer) { + if (!closer) + return; + let list = this._closers.get(path); + if (!list) { + list = []; + this._closers.set(path, list); + } + list.push(closer); + } + _readdirp(root, opts) { + if (this.closed) + return; + const options = {type: EV_ALL, alwaysStat: true, lstat: true, ...opts}; + let stream = readdirp(root, options); + this._streams.add(stream); + stream.once(STR_CLOSE, () => { + stream = void 0; + }); + stream.once(STR_END, () => { + if (stream) { + this._streams.delete(stream); + stream = void 0; + } + }); + return stream; + } + }; + exports2.FSWatcher = FSWatcher; + var watch = (paths, options) => { + const watcher = new FSWatcher(options); + watcher.add(paths); + return watcher; + }; + exports2.watch = watch; +}); + +// ../../node_modules/nunjucks/src/node-loaders.js +var require_node_loaders = __commonJS((exports2, module2) => { + "use strict"; + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var fs = require("fs"); + var path = require("path"); + var Loader2 = require_loader(); + var _require2 = require_precompiled_loader(); + var PrecompiledLoader = _require2.PrecompiledLoader; + var chokidar; + var FileSystemLoader = /* @__PURE__ */ function(_Loader) { + _inheritsLoose(FileSystemLoader2, _Loader); + function FileSystemLoader2(searchPaths, opts) { + var _this; + _this = _Loader.call(this) || this; + if (typeof opts === "boolean") { + console.log("[nunjucks] Warning: you passed a boolean as the second argument to FileSystemLoader, but it now takes an options object. See http://mozilla.github.io/nunjucks/api.html#filesystemloader"); + } + opts = opts || {}; + _this.pathsToNames = {}; + _this.noCache = !!opts.noCache; + if (searchPaths) { + searchPaths = Array.isArray(searchPaths) ? searchPaths : [searchPaths]; + _this.searchPaths = searchPaths.map(path.normalize); + } else { + _this.searchPaths = ["."]; + } + if (opts.watch) { + try { + chokidar = require_chokidar(); + } catch (e2) { + throw new Error("watch requires chokidar to be installed"); + } + var paths = _this.searchPaths.filter(fs.existsSync); + var watcher = chokidar.watch(paths); + watcher.on("all", function(event, fullname) { + fullname = path.resolve(fullname); + if (event === "change" && fullname in _this.pathsToNames) { + _this.emit("update", _this.pathsToNames[fullname], fullname); + } + }); + watcher.on("error", function(error) { + console.log("Watcher error: " + error); + }); + } + return _this; + } + var _proto = FileSystemLoader2.prototype; + _proto.getSource = function getSource(name) { + var fullpath = null; + var paths = this.searchPaths; + for (var i = 0; i < paths.length; i++) { + var basePath = path.resolve(paths[i]); + var p = path.resolve(paths[i], name); + if (p.indexOf(basePath) === 0 && fs.existsSync(p)) { + fullpath = p; + break; + } + } + if (!fullpath) { + return null; + } + this.pathsToNames[fullpath] = name; + var source = { + src: fs.readFileSync(fullpath, "utf-8"), + path: fullpath, + noCache: this.noCache + }; + this.emit("load", name, source); + return source; + }; + return FileSystemLoader2; + }(Loader2); + var NodeResolveLoader = /* @__PURE__ */ function(_Loader2) { + _inheritsLoose(NodeResolveLoader2, _Loader2); + function NodeResolveLoader2(opts) { + var _this2; + _this2 = _Loader2.call(this) || this; + opts = opts || {}; + _this2.pathsToNames = {}; + _this2.noCache = !!opts.noCache; + if (opts.watch) { + try { + chokidar = require_chokidar(); + } catch (e2) { + throw new Error("watch requires chokidar to be installed"); + } + _this2.watcher = chokidar.watch(); + _this2.watcher.on("change", function(fullname) { + _this2.emit("update", _this2.pathsToNames[fullname], fullname); + }); + _this2.watcher.on("error", function(error) { + console.log("Watcher error: " + error); + }); + _this2.on("load", function(name, source) { + _this2.watcher.add(source.path); + }); + } + return _this2; + } + var _proto2 = NodeResolveLoader2.prototype; + _proto2.getSource = function getSource(name) { + if (/^\.?\.?(\/|\\)/.test(name)) { + return null; + } + if (/^[A-Z]:/.test(name)) { + return null; + } + var fullpath; + try { + fullpath = require.resolve(name); + } catch (e2) { + return null; + } + this.pathsToNames[fullpath] = name; + var source = { + src: fs.readFileSync(fullpath, "utf-8"), + path: fullpath, + noCache: this.noCache + }; + this.emit("load", name, source); + return source; + }; + return NodeResolveLoader2; + }(Loader2); + module2.exports = { + FileSystemLoader, + PrecompiledLoader, + NodeResolveLoader + }; +}); + +// ../../node_modules/nunjucks/src/loaders.js +var require_loaders = __commonJS((exports2, module2) => { + "use strict"; + module2.exports = require_node_loaders(); +}); + +// ../../node_modules/nunjucks/src/tests.js +var require_tests = __commonJS((exports2) => { + "use strict"; + var SafeString = require_runtime().SafeString; + function callable(value) { + return typeof value === "function"; + } + exports2.callable = callable; + function defined(value) { + return value !== void 0; + } + exports2.defined = defined; + function divisibleby(one, two) { + return one % two === 0; + } + exports2.divisibleby = divisibleby; + function escaped(value) { + return value instanceof SafeString; + } + exports2.escaped = escaped; + function equalto(one, two) { + return one === two; + } + exports2.equalto = equalto; + exports2.eq = exports2.equalto; + exports2.sameas = exports2.equalto; + function even(value) { + return value % 2 === 0; + } + exports2.even = even; + function falsy(value) { + return !value; + } + exports2.falsy = falsy; + function ge(one, two) { + return one >= two; + } + exports2.ge = ge; + function greaterthan(one, two) { + return one > two; + } + exports2.greaterthan = greaterthan; + exports2.gt = exports2.greaterthan; + function le(one, two) { + return one <= two; + } + exports2.le = le; + function lessthan(one, two) { + return one < two; + } + exports2.lessthan = lessthan; + exports2.lt = exports2.lessthan; + function lower(value) { + return value.toLowerCase() === value; + } + exports2.lower = lower; + function ne(one, two) { + return one !== two; + } + exports2.ne = ne; + function nullTest(value) { + return value === null; + } + exports2.null = nullTest; + function number(value) { + return typeof value === "number"; + } + exports2.number = number; + function odd(value) { + return value % 2 === 1; + } + exports2.odd = odd; + function string(value) { + return typeof value === "string"; + } + exports2.string = string; + function truthy(value) { + return !!value; + } + exports2.truthy = truthy; + function undefinedTest(value) { + return value === void 0; + } + exports2.undefined = undefinedTest; + function upper(value) { + return value.toUpperCase() === value; + } + exports2.upper = upper; + function iterable(value) { + if (typeof Symbol !== "undefined") { + return !!value[Symbol.iterator]; + } else { + return Array.isArray(value) || typeof value === "string"; + } + } + exports2.iterable = iterable; + function mapping(value) { + var bool = value !== null && value !== void 0 && typeof value === "object" && !Array.isArray(value); + if (Set) { + return bool && !(value instanceof Set); + } else { + return bool; + } + } + exports2.mapping = mapping; +}); + +// ../../node_modules/nunjucks/src/globals.js +var require_globals = __commonJS((exports2, module2) => { + "use strict"; + function _cycler(items) { + var index = -1; + return { + current: null, + reset: function reset2() { + index = -1; + this.current = null; + }, + next: function next() { + index++; + if (index >= items.length) { + index = 0; + } + this.current = items[index]; + return this.current; + } + }; + } + function _joiner(sep) { + sep = sep || ","; + var first = true; + return function() { + var val = first ? "" : sep; + first = false; + return val; + }; + } + function globals() { + return { + range: function range(start, stop, step) { + if (typeof stop === "undefined") { + stop = start; + start = 0; + step = 1; + } else if (!step) { + step = 1; + } + var arr = []; + if (step > 0) { + for (var i = start; i < stop; i += step) { + arr.push(i); + } + } else { + for (var _i = start; _i > stop; _i += step) { + arr.push(_i); + } + } + return arr; + }, + cycler: function cycler() { + return _cycler(Array.prototype.slice.call(arguments)); + }, + joiner: function joiner(sep) { + return _joiner(sep); + } + }; + } + module2.exports = globals; +}); + +// ../../node_modules/nunjucks/src/express-app.js +var require_express_app = __commonJS((exports2, module2) => { + "use strict"; + var path = require("path"); + module2.exports = function express(env, app) { + function NunjucksView(name, opts) { + this.name = name; + this.path = name; + this.defaultEngine = opts.defaultEngine; + this.ext = path.extname(name); + if (!this.ext && !this.defaultEngine) { + throw new Error("No default engine was specified and no extension was provided."); + } + if (!this.ext) { + this.name += this.ext = (this.defaultEngine[0] !== "." ? "." : "") + this.defaultEngine; + } + } + NunjucksView.prototype.render = function render2(opts, cb) { + env.render(this.name, opts, cb); + }; + app.set("view", NunjucksView); + app.set("nunjucksEnv", env); + return env; + }; +}); + +// ../../node_modules/nunjucks/src/environment.js +var require_environment = __commonJS((exports2, module2) => { + "use strict"; + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + _setPrototypeOf(subClass, superClass); + } + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf2(o2, p2) { + o2.__proto__ = p2; + return o2; + }; + return _setPrototypeOf(o, p); + } + var asap = require_asap(); + var _waterfall = require_a_sync_waterfall(); + var lib2 = require_lib(); + var compiler2 = require_compiler(); + var filters = require_filters(); + var _require2 = require_loaders(); + var FileSystemLoader = _require2.FileSystemLoader; + var WebLoader = _require2.WebLoader; + var PrecompiledLoader = _require2.PrecompiledLoader; + var tests = require_tests(); + var globals = require_globals(); + var _require22 = require_object(); + var Obj = _require22.Obj; + var EmitterObj = _require22.EmitterObj; + var globalRuntime = require_runtime(); + var handleError = globalRuntime.handleError; + var Frame = globalRuntime.Frame; + var expressApp = require_express_app(); + function callbackAsap(cb, err, res) { + asap(function() { + cb(err, res); + }); + } + var noopTmplSrc = { + type: "code", + obj: { + root: function root(env, context, frame, runtime2, cb) { + try { + cb(null, ""); + } catch (e2) { + cb(handleError(e2, null, null)); + } + } + } + }; + var Environment2 = /* @__PURE__ */ function(_EmitterObj) { + _inheritsLoose(Environment3, _EmitterObj); + function Environment3() { + return _EmitterObj.apply(this, arguments) || this; + } + var _proto = Environment3.prototype; + _proto.init = function init(loaders2, opts) { + var _this = this; + opts = this.opts = opts || {}; + this.opts.dev = !!opts.dev; + this.opts.autoescape = opts.autoescape != null ? opts.autoescape : true; + this.opts.throwOnUndefined = !!opts.throwOnUndefined; + this.opts.trimBlocks = !!opts.trimBlocks; + this.opts.lstripBlocks = !!opts.lstripBlocks; + this.loaders = []; + if (!loaders2) { + if (FileSystemLoader) { + this.loaders = [new FileSystemLoader("views")]; + } else if (WebLoader) { + this.loaders = [new WebLoader("/views")]; + } + } else { + this.loaders = lib2.isArray(loaders2) ? loaders2 : [loaders2]; + } + if (typeof window !== "undefined" && window.nunjucksPrecompiled) { + this.loaders.unshift(new PrecompiledLoader(window.nunjucksPrecompiled)); + } + this._initLoaders(); + this.globals = globals(); + this.filters = {}; + this.tests = {}; + this.asyncFilters = []; + this.extensions = {}; + this.extensionsList = []; + lib2._entries(filters).forEach(function(_ref) { + var name = _ref[0], filter = _ref[1]; + return _this.addFilter(name, filter); + }); + lib2._entries(tests).forEach(function(_ref2) { + var name = _ref2[0], test = _ref2[1]; + return _this.addTest(name, test); + }); + }; + _proto._initLoaders = function _initLoaders() { + var _this2 = this; + this.loaders.forEach(function(loader) { + loader.cache = {}; + if (typeof loader.on === "function") { + loader.on("update", function(name, fullname) { + loader.cache[name] = null; + _this2.emit("update", name, fullname, loader); + }); + loader.on("load", function(name, source) { + _this2.emit("load", name, source, loader); + }); + } + }); + }; + _proto.invalidateCache = function invalidateCache() { + this.loaders.forEach(function(loader) { + loader.cache = {}; + }); + }; + _proto.addExtension = function addExtension(name, extension) { + extension.__name = name; + this.extensions[name] = extension; + this.extensionsList.push(extension); + return this; + }; + _proto.removeExtension = function removeExtension(name) { + var extension = this.getExtension(name); + if (!extension) { + return; + } + this.extensionsList = lib2.without(this.extensionsList, extension); + delete this.extensions[name]; + }; + _proto.getExtension = function getExtension(name) { + return this.extensions[name]; + }; + _proto.hasExtension = function hasExtension(name) { + return !!this.extensions[name]; + }; + _proto.addGlobal = function addGlobal(name, value) { + this.globals[name] = value; + return this; + }; + _proto.getGlobal = function getGlobal(name) { + if (typeof this.globals[name] === "undefined") { + throw new Error("global not found: " + name); + } + return this.globals[name]; + }; + _proto.addFilter = function addFilter(name, func, async) { + var wrapped = func; + if (async) { + this.asyncFilters.push(name); + } + this.filters[name] = wrapped; + return this; + }; + _proto.getFilter = function getFilter(name) { + if (!this.filters[name]) { + throw new Error("filter not found: " + name); + } + return this.filters[name]; + }; + _proto.addTest = function addTest(name, func) { + this.tests[name] = func; + return this; + }; + _proto.getTest = function getTest(name) { + if (!this.tests[name]) { + throw new Error("test not found: " + name); + } + return this.tests[name]; + }; + _proto.resolveTemplate = function resolveTemplate(loader, parentName, filename) { + var isRelative = loader.isRelative && parentName ? loader.isRelative(filename) : false; + return isRelative && loader.resolve ? loader.resolve(parentName, filename) : filename; + }; + _proto.getTemplate = function getTemplate(name, eagerCompile, parentName, ignoreMissing, cb) { + var _this3 = this; + var that = this; + var tmpl = null; + if (name && name.raw) { + name = name.raw; + } + if (lib2.isFunction(parentName)) { + cb = parentName; + parentName = null; + eagerCompile = eagerCompile || false; + } + if (lib2.isFunction(eagerCompile)) { + cb = eagerCompile; + eagerCompile = false; + } + if (name instanceof Template2) { + tmpl = name; + } else if (typeof name !== "string") { + throw new Error("template names must be a string: " + name); + } else { + for (var i = 0; i < this.loaders.length; i++) { + var loader = this.loaders[i]; + tmpl = loader.cache[this.resolveTemplate(loader, parentName, name)]; + if (tmpl) { + break; + } + } + } + if (tmpl) { + if (eagerCompile) { + tmpl.compile(); + } + if (cb) { + cb(null, tmpl); + return void 0; + } else { + return tmpl; + } + } + var syncResult; + var createTemplate = function createTemplate2(err, info) { + if (!info && !err && !ignoreMissing) { + err = new Error("template not found: " + name); + } + if (err) { + if (cb) { + cb(err); + return; + } else { + throw err; + } + } + var newTmpl; + if (!info) { + newTmpl = new Template2(noopTmplSrc, _this3, "", eagerCompile); + } else { + newTmpl = new Template2(info.src, _this3, info.path, eagerCompile); + if (!info.noCache) { + info.loader.cache[name] = newTmpl; + } + } + if (cb) { + cb(null, newTmpl); + } else { + syncResult = newTmpl; + } + }; + lib2.asyncIter(this.loaders, function(loader2, i2, next, done) { + function handle(err, src) { + if (err) { + done(err); + } else if (src) { + src.loader = loader2; + done(null, src); + } else { + next(); + } + } + name = that.resolveTemplate(loader2, parentName, name); + if (loader2.async) { + loader2.getSource(name, handle); + } else { + handle(null, loader2.getSource(name)); + } + }, createTemplate); + return syncResult; + }; + _proto.express = function express(app) { + return expressApp(this, app); + }; + _proto.render = function render2(name, ctx, cb) { + if (lib2.isFunction(ctx)) { + cb = ctx; + ctx = null; + } + var syncResult = null; + this.getTemplate(name, function(err, tmpl) { + if (err && cb) { + callbackAsap(cb, err); + } else if (err) { + throw err; + } else { + syncResult = tmpl.render(ctx, cb); + } + }); + return syncResult; + }; + _proto.renderString = function renderString2(src, ctx, opts, cb) { + if (lib2.isFunction(opts)) { + cb = opts; + opts = {}; + } + opts = opts || {}; + var tmpl = new Template2(src, this, opts.path); + return tmpl.render(ctx, cb); + }; + _proto.waterfall = function waterfall(tasks, callback, forceAsync) { + return _waterfall(tasks, callback, forceAsync); + }; + return Environment3; + }(EmitterObj); + var Context = /* @__PURE__ */ function(_Obj) { + _inheritsLoose(Context2, _Obj); + function Context2() { + return _Obj.apply(this, arguments) || this; + } + var _proto2 = Context2.prototype; + _proto2.init = function init(ctx, blocks, env) { + var _this4 = this; + this.env = env || new Environment2(); + this.ctx = lib2.extend({}, ctx); + this.blocks = {}; + this.exported = []; + lib2.keys(blocks).forEach(function(name) { + _this4.addBlock(name, blocks[name]); + }); + }; + _proto2.lookup = function lookup(name) { + if (name in this.env.globals && !(name in this.ctx)) { + return this.env.globals[name]; + } else { + return this.ctx[name]; + } + }; + _proto2.setVariable = function setVariable(name, val) { + this.ctx[name] = val; + }; + _proto2.getVariables = function getVariables() { + return this.ctx; + }; + _proto2.addBlock = function addBlock(name, block) { + this.blocks[name] = this.blocks[name] || []; + this.blocks[name].push(block); + return this; + }; + _proto2.getBlock = function getBlock(name) { + if (!this.blocks[name]) { + throw new Error('unknown block "' + name + '"'); + } + return this.blocks[name][0]; + }; + _proto2.getSuper = function getSuper(env, name, block, frame, runtime2, cb) { + var idx = lib2.indexOf(this.blocks[name] || [], block); + var blk = this.blocks[name][idx + 1]; + var context = this; + if (idx === -1 || !blk) { + throw new Error('no super block available for "' + name + '"'); + } + blk(env, context, frame, runtime2, cb); + }; + _proto2.addExport = function addExport(name) { + this.exported.push(name); + }; + _proto2.getExported = function getExported() { + var _this5 = this; + var exported = {}; + this.exported.forEach(function(name) { + exported[name] = _this5.ctx[name]; + }); + return exported; + }; + return Context2; + }(Obj); + var Template2 = /* @__PURE__ */ function(_Obj2) { + _inheritsLoose(Template3, _Obj2); + function Template3() { + return _Obj2.apply(this, arguments) || this; + } + var _proto3 = Template3.prototype; + _proto3.init = function init(src, env, path, eagerCompile) { + this.env = env || new Environment2(); + if (lib2.isObject(src)) { + switch (src.type) { + case "code": + this.tmplProps = src.obj; + break; + case "string": + this.tmplStr = src.obj; + break; + default: + throw new Error("Unexpected template object type " + src.type + "; expected 'code', or 'string'"); + } + } else if (lib2.isString(src)) { + this.tmplStr = src; + } else { + throw new Error("src must be a string or an object describing the source"); + } + this.path = path; + if (eagerCompile) { + try { + this._compile(); + } catch (err) { + throw lib2._prettifyError(this.path, this.env.opts.dev, err); + } + } else { + this.compiled = false; + } + }; + _proto3.render = function render2(ctx, parentFrame, cb) { + var _this6 = this; + if (typeof ctx === "function") { + cb = ctx; + ctx = {}; + } else if (typeof parentFrame === "function") { + cb = parentFrame; + parentFrame = null; + } + var forceAsync = !parentFrame; + try { + this.compile(); + } catch (e2) { + var err = lib2._prettifyError(this.path, this.env.opts.dev, e2); + if (cb) { + return callbackAsap(cb, err); + } else { + throw err; + } + } + var context = new Context(ctx || {}, this.blocks, this.env); + var frame = parentFrame ? parentFrame.push(true) : new Frame(); + frame.topLevel = true; + var syncResult = null; + var didError = false; + this.rootRenderFunc(this.env, context, frame, globalRuntime, function(err2, res) { + if (didError && cb && typeof res !== "undefined") { + return; + } + if (err2) { + err2 = lib2._prettifyError(_this6.path, _this6.env.opts.dev, err2); + didError = true; + } + if (cb) { + if (forceAsync) { + callbackAsap(cb, err2, res); + } else { + cb(err2, res); + } + } else { + if (err2) { + throw err2; + } + syncResult = res; + } + }); + return syncResult; + }; + _proto3.getExported = function getExported(ctx, parentFrame, cb) { + if (typeof ctx === "function") { + cb = ctx; + ctx = {}; + } + if (typeof parentFrame === "function") { + cb = parentFrame; + parentFrame = null; + } + try { + this.compile(); + } catch (e2) { + if (cb) { + return cb(e2); + } else { + throw e2; + } + } + var frame = parentFrame ? parentFrame.push() : new Frame(); + frame.topLevel = true; + var context = new Context(ctx || {}, this.blocks, this.env); + this.rootRenderFunc(this.env, context, frame, globalRuntime, function(err) { + if (err) { + cb(err, null); + } else { + cb(null, context.getExported()); + } + }); + }; + _proto3.compile = function compile2() { + if (!this.compiled) { + this._compile(); + } + }; + _proto3._compile = function _compile() { + var props; + if (this.tmplProps) { + props = this.tmplProps; + } else { + var source = compiler2.compile(this.tmplStr, this.env.asyncFilters, this.env.extensionsList, this.path, this.env.opts); + var func = new Function(source); + props = func(); + } + this.blocks = this._getBlocks(props); + this.rootRenderFunc = props.root; + this.compiled = true; + }; + _proto3._getBlocks = function _getBlocks(props) { + var blocks = {}; + lib2.keys(props).forEach(function(k) { + if (k.slice(0, 2) === "b_") { + blocks[k.slice(2)] = props[k]; + } + }); + return blocks; + }; + return Template3; + }(Obj); + module2.exports = { + Environment: Environment2, + Template: Template2 + }; +}); + +// ../../node_modules/nunjucks/src/precompile-global.js +var require_precompile_global = __commonJS((exports2, module2) => { + "use strict"; + function precompileGlobal(templates, opts) { + var out = ""; + opts = opts || {}; + for (var i = 0; i < templates.length; i++) { + var name = JSON.stringify(templates[i].name); + var template = templates[i].template; + out += "(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})[" + name + "] = (function() {\n" + template + "\n})();\n"; + if (opts.asFunction) { + out += "return function(ctx, cb) { return nunjucks.render(" + name + ", ctx, cb); }\n"; + } + out += "})();\n"; + } + return out; + } + module2.exports = precompileGlobal; +}); + +// ../../node_modules/nunjucks/src/precompile.js +var require_precompile = __commonJS((exports2, module2) => { + "use strict"; + var fs = require("fs"); + var path = require("path"); + var _require2 = require_lib(); + var _prettifyError = _require2._prettifyError; + var compiler2 = require_compiler(); + var _require22 = require_environment(); + var Environment2 = _require22.Environment; + var precompileGlobal = require_precompile_global(); + function match(filename, patterns) { + if (!Array.isArray(patterns)) { + return false; + } + return patterns.some(function(pattern) { + return filename.match(pattern); + }); + } + function precompileString(str, opts) { + opts = opts || {}; + opts.isString = true; + var env = opts.env || new Environment2([]); + var wrapper = opts.wrapper || precompileGlobal; + if (!opts.name) { + throw new Error('the "name" option is required when compiling a string'); + } + return wrapper([_precompile(str, opts.name, env)], opts); + } + function precompile2(input, opts) { + opts = opts || {}; + var env = opts.env || new Environment2([]); + var wrapper = opts.wrapper || precompileGlobal; + if (opts.isString) { + return precompileString(input, opts); + } + var pathStats = fs.existsSync(input) && fs.statSync(input); + var precompiled = []; + var templates = []; + function addTemplates(dir) { + fs.readdirSync(dir).forEach(function(file) { + var filepath = path.join(dir, file); + var subpath = filepath.substr(path.join(input, "/").length); + var stat = fs.statSync(filepath); + if (stat && stat.isDirectory()) { + subpath += "/"; + if (!match(subpath, opts.exclude)) { + addTemplates(filepath); + } + } else if (match(subpath, opts.include)) { + templates.push(filepath); + } + }); + } + if (pathStats.isFile()) { + precompiled.push(_precompile(fs.readFileSync(input, "utf-8"), opts.name || input, env)); + } else if (pathStats.isDirectory()) { + addTemplates(input); + for (var i = 0; i < templates.length; i++) { + var name = templates[i].replace(path.join(input, "/"), ""); + try { + precompiled.push(_precompile(fs.readFileSync(templates[i], "utf-8"), name, env)); + } catch (e2) { + if (opts.force) { + console.error(e2); + } else { + throw e2; + } + } + } + } + return wrapper(precompiled, opts); + } + function _precompile(str, name, env) { + env = env || new Environment2([]); + var asyncFilters = env.asyncFilters; + var extensions = env.extensionsList; + var template; + name = name.replace(/\\/g, "/"); + try { + template = compiler2.compile(str, asyncFilters, extensions, name, env.opts); + } catch (err) { + throw _prettifyError(name, false, err); + } + return { + name, + template + }; + } + module2.exports = { + precompile: precompile2, + precompileString + }; +}); + +// ../../node_modules/nunjucks/src/jinja-compat.js +var require_jinja_compat = __commonJS((exports2, module2) => { + "use strict"; + function installCompat() { + "use strict"; + var runtime2 = this.runtime; + var lib2 = this.lib; + var Compiler = this.compiler.Compiler; + var Parser = this.parser.Parser; + var nodes2 = this.nodes; + var lexer2 = this.lexer; + var orig_contextOrFrameLookup = runtime2.contextOrFrameLookup; + var orig_memberLookup = runtime2.memberLookup; + var orig_Compiler_assertType; + var orig_Parser_parseAggregate; + if (Compiler) { + orig_Compiler_assertType = Compiler.prototype.assertType; + } + if (Parser) { + orig_Parser_parseAggregate = Parser.prototype.parseAggregate; + } + function uninstall() { + runtime2.contextOrFrameLookup = orig_contextOrFrameLookup; + runtime2.memberLookup = orig_memberLookup; + if (Compiler) { + Compiler.prototype.assertType = orig_Compiler_assertType; + } + if (Parser) { + Parser.prototype.parseAggregate = orig_Parser_parseAggregate; + } + } + runtime2.contextOrFrameLookup = function contextOrFrameLookup(context, frame, key) { + var val = orig_contextOrFrameLookup.apply(this, arguments); + if (val !== void 0) { + return val; + } + switch (key) { + case "True": + return true; + case "False": + return false; + case "None": + return null; + default: + return void 0; + } + }; + function getTokensState(tokens) { + return { + index: tokens.index, + lineno: tokens.lineno, + colno: tokens.colno + }; + } + if (process.env.BUILD_TYPE !== "SLIM" && nodes2 && Compiler && Parser) { + var Slice = nodes2.Node.extend("Slice", { + fields: ["start", "stop", "step"], + init: function init(lineno, colno, start, stop, step) { + start = start || new nodes2.Literal(lineno, colno, null); + stop = stop || new nodes2.Literal(lineno, colno, null); + step = step || new nodes2.Literal(lineno, colno, 1); + this.parent(lineno, colno, start, stop, step); + } + }); + Compiler.prototype.assertType = function assertType(node) { + if (node instanceof Slice) { + return; + } + orig_Compiler_assertType.apply(this, arguments); + }; + Compiler.prototype.compileSlice = function compileSlice(node, frame) { + this._emit("("); + this._compileExpression(node.start, frame); + this._emit("),("); + this._compileExpression(node.stop, frame); + this._emit("),("); + this._compileExpression(node.step, frame); + this._emit(")"); + }; + Parser.prototype.parseAggregate = function parseAggregate() { + var _this = this; + var origState = getTokensState(this.tokens); + origState.colno--; + origState.index--; + try { + return orig_Parser_parseAggregate.apply(this); + } catch (e2) { + var errState = getTokensState(this.tokens); + var rethrow = function rethrow2() { + lib2._assign(_this.tokens, errState); + return e2; + }; + lib2._assign(this.tokens, origState); + this.peeked = false; + var tok = this.peekToken(); + if (tok.type !== lexer2.TOKEN_LEFT_BRACKET) { + throw rethrow(); + } else { + this.nextToken(); + } + var node = new Slice(tok.lineno, tok.colno); + var isSlice = false; + for (var i = 0; i <= node.fields.length; i++) { + if (this.skip(lexer2.TOKEN_RIGHT_BRACKET)) { + break; + } + if (i === node.fields.length) { + if (isSlice) { + this.fail("parseSlice: too many slice components", tok.lineno, tok.colno); + } else { + break; + } + } + if (this.skip(lexer2.TOKEN_COLON)) { + isSlice = true; + } else { + var field = node.fields[i]; + node[field] = this.parseExpression(); + isSlice = this.skip(lexer2.TOKEN_COLON) || isSlice; + } + } + if (!isSlice) { + throw rethrow(); + } + return new nodes2.Array(tok.lineno, tok.colno, [node]); + } + }; + } + function sliceLookup(obj, start, stop, step) { + obj = obj || []; + if (start === null) { + start = step < 0 ? obj.length - 1 : 0; + } + if (stop === null) { + stop = step < 0 ? -1 : obj.length; + } else if (stop < 0) { + stop += obj.length; + } + if (start < 0) { + start += obj.length; + } + var results = []; + for (var i = start; ; i += step) { + if (i < 0 || i > obj.length) { + break; + } + if (step > 0 && i >= stop) { + break; + } + if (step < 0 && i <= stop) { + break; + } + results.push(runtime2.memberLookup(obj, i)); + } + return results; + } + function hasOwnProp(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); + } + var ARRAY_MEMBERS = { + pop: function pop(index) { + if (index === void 0) { + return this.pop(); + } + if (index >= this.length || index < 0) { + throw new Error("KeyError"); + } + return this.splice(index, 1); + }, + append: function append(element) { + return this.push(element); + }, + remove: function remove(element) { + for (var i = 0; i < this.length; i++) { + if (this[i] === element) { + return this.splice(i, 1); + } + } + throw new Error("ValueError"); + }, + count: function count(element) { + var count2 = 0; + for (var i = 0; i < this.length; i++) { + if (this[i] === element) { + count2++; + } + } + return count2; + }, + index: function index(element) { + var i; + if ((i = this.indexOf(element)) === -1) { + throw new Error("ValueError"); + } + return i; + }, + find: function find(element) { + return this.indexOf(element); + }, + insert: function insert(index, elem) { + return this.splice(index, 0, elem); + } + }; + var OBJECT_MEMBERS = { + items: function items() { + return lib2._entries(this); + }, + values: function values() { + return lib2._values(this); + }, + keys: function keys() { + return lib2.keys(this); + }, + get: function get(key, def) { + var output = this[key]; + if (output === void 0) { + output = def; + } + return output; + }, + has_key: function has_key(key) { + return hasOwnProp(this, key); + }, + pop: function pop(key, def) { + var output = this[key]; + if (output === void 0 && def !== void 0) { + output = def; + } else if (output === void 0) { + throw new Error("KeyError"); + } else { + delete this[key]; + } + return output; + }, + popitem: function popitem() { + var keys = lib2.keys(this); + if (!keys.length) { + throw new Error("KeyError"); + } + var k = keys[0]; + var val = this[k]; + delete this[k]; + return [k, val]; + }, + setdefault: function setdefault(key, def) { + if (def === void 0) { + def = null; + } + if (!(key in this)) { + this[key] = def; + } + return this[key]; + }, + update: function update(kwargs) { + lib2._assign(this, kwargs); + return null; + } + }; + OBJECT_MEMBERS.iteritems = OBJECT_MEMBERS.items; + OBJECT_MEMBERS.itervalues = OBJECT_MEMBERS.values; + OBJECT_MEMBERS.iterkeys = OBJECT_MEMBERS.keys; + runtime2.memberLookup = function memberLookup(obj, val, autoescape) { + if (arguments.length === 4) { + return sliceLookup.apply(this, arguments); + } + obj = obj || {}; + if (lib2.isArray(obj) && hasOwnProp(ARRAY_MEMBERS, val)) { + return ARRAY_MEMBERS[val].bind(obj); + } + if (lib2.isObject(obj) && hasOwnProp(OBJECT_MEMBERS, val)) { + return OBJECT_MEMBERS[val].bind(obj); + } + return orig_memberLookup.apply(this, arguments); + }; + return uninstall; + } + module2.exports = installCompat; +}); + +// ../../node_modules/nunjucks/index.js +"use strict"; +var lib = require_lib(); +var _require = require_environment(); +var Environment = _require.Environment; +var Template = _require.Template; +var Loader = require_loader(); +var loaders = require_loaders(); +var precompile = require_precompile(); +var compiler = require_compiler(); +var parser = require_parser(); +var lexer = require_lexer(); +var runtime = require_runtime(); +var nodes = require_nodes(); +var installJinjaCompat = require_jinja_compat(); +var e; +function configure(templatesPath, opts) { + opts = opts || {}; + if (lib.isObject(templatesPath)) { + opts = templatesPath; + templatesPath = null; + } + var TemplateLoader; + if (loaders.FileSystemLoader) { + TemplateLoader = new loaders.FileSystemLoader(templatesPath, { + watch: opts.watch, + noCache: opts.noCache + }); + } else if (loaders.WebLoader) { + TemplateLoader = new loaders.WebLoader(templatesPath, { + useCache: opts.web && opts.web.useCache, + async: opts.web && opts.web.async + }); + } + e = new Environment(TemplateLoader, opts); + if (opts && opts.express) { + e.express(opts.express); + } + return e; +} +module.exports = { + Environment, + Template, + Loader, + FileSystemLoader: loaders.FileSystemLoader, + NodeResolveLoader: loaders.NodeResolveLoader, + PrecompiledLoader: loaders.PrecompiledLoader, + WebLoader: loaders.WebLoader, + compiler, + parser, + lexer, + runtime, + lib, + nodes, + installJinjaCompat, + configure, + reset: function reset() { + e = void 0; + }, + compile: function compile(src, env, path, eagerCompile) { + if (!e) { + configure(); + } + return new Template(src, env, path, eagerCompile); + }, + render: function render(name, ctx, cb) { + if (!e) { + configure(); + } + return e.render(name, ctx, cb); + }, + renderString: function renderString(src, ctx, cb) { + if (!e) { + configure(); + } + return e.renderString(src, ctx, cb); + }, + precompile: precompile ? precompile.precompile : void 0, + precompileString: precompile ? precompile.precompileString : void 0 +}; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 374cfeffd6..602028f9b3 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.13", + "version": "0.15.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,21 +27,22 @@ "test": "backstage-cli test", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "clean": "backstage-cli clean", + "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/errors": "^0.1.4", + "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.9", - "@backstage/plugin-catalog-backend": "^0.17.4", + "@backstage/plugin-catalog-backend": "^0.18.0", "@backstage/plugin-scaffolder-common": "^0.1.1", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.4", "@backstage/types": "^0.1.1", - "@gitbeaker/core": "^30.2.0", - "@gitbeaker/node": "^30.2.0", + "@gitbeaker/core": "^34.6.0", + "@gitbeaker/node": "^34.6.0", "@octokit/rest": "^18.5.3", "@octokit/webhooks": "^9.14.1", "@types/express": "^4.17.6", @@ -49,7 +50,6 @@ "command-exists": "^1.2.9", "compression": "^1.7.4", "cors": "^2.8.5", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "10.0.0", @@ -64,21 +64,24 @@ "lodash": "^4.17.21", "luxon": "^2.0.2", "morgan": "^1.10.0", + "node-fetch": "^2.6.1", "nunjucks": "^3.2.3", "octokit-plugin-create-pull-request": "^3.10.0", "uuid": "^8.2.0", "winston": "^3.2.1", - "yaml": "^1.10.0" + "yaml": "^1.10.0", + "vm2": "^3.9.5" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/test-utils": "^0.1.23", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/mock-fs": "^4.13.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", + "esbuild": "^0.13.14", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", "msw": "^0.35.0", @@ -88,7 +91,8 @@ "files": [ "dist", "migrations", - "config.d.ts" + "config.d.ts", + "assets" ], "configSchema": "config.d.ts" } diff --git a/plugins/scaffolder-backend/scripts/build-nunjucks.js b/plugins/scaffolder-backend/scripts/build-nunjucks.js new file mode 100755 index 0000000000..b195f96159 --- /dev/null +++ b/plugins/scaffolder-backend/scripts/build-nunjucks.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node +/* + * 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. + */ + +/* eslint-disable no-restricted-syntax */ +/* eslint-disable import/no-extraneous-dependencies */ + +const path = require('path'); + +const NUNJUCKS_LICENSE = `/** + * Copyright (c) 2012-2015, James Long + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +`; + +// This script is used to bundle nunjucks into a single script that is +// loaded into a sandbox for executing templates. +require('esbuild') + .build({ + entryPoints: [require.resolve('nunjucks')], + bundle: true, + format: 'cjs', + platform: 'node', + target: 'node14', + banner: { js: NUNJUCKS_LICENSE }, + external: ['fsevents'], + outfile: path.resolve(__dirname, '../assets/nunjucks.js.txt'), + }) + .catch(err => { + console.log(err.stack); + process.exit(1); + }); diff --git a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts new file mode 100644 index 0000000000..134b877efe --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { SecureTemplater } from './SecureTemplater'; + +describe('SecureTemplater', () => { + it('should render some templates', async () => { + const render = await SecureTemplater.loadRenderer(); + expect(render('${{ test }}', { test: 'my-value' })).toBe('my-value'); + + expect(render('${{ test | dump }}', { test: 'my-value' })).toBe( + '"my-value"', + ); + + expect( + render('${{ test | replace("my-", "our-") }}', { + test: 'my-value', + }), + ).toBe('our-value'); + + expect(() => + render('${{ invalid...syntax }}', { + test: 'my-value', + }), + ).toThrow(/expected name as lookup value, got ./); + }); + + it('should make cookiecutter compatibility available when requested', async () => { + const renderWith = await SecureTemplater.loadRenderer({ + cookiecutterCompat: true, + }); + const renderWithout = await SecureTemplater.loadRenderer(); + + // Same two tests repeated to make sure switching back and forth works + expect(renderWith('{{ 1 | jsonify }}', {})).toBe('1'); + expect(renderWith('{{ 1 | jsonify }}', {})).toBe('1'); + expect(() => renderWithout('${{ 1 | jsonify }}', {})).toThrow( + /Error: filter not found: jsonify/, + ); + expect(renderWith('{{ 1 | jsonify }}', {})).toBe('1'); + expect(() => renderWithout('${{ 1 | jsonify }}', {})).toThrow( + /Error: filter not found: jsonify/, + ); + expect(() => renderWithout('${{ 1 | jsonify }}', {})).toThrow( + /Error: filter not found: jsonify/, + ); + expect(() => renderWithout('${{ 1 | jsonify }}', {})).toThrow( + /Error: filter not found: jsonify/, + ); + expect(renderWith('{{ 1 | jsonify }}', {})).toBe('1'); + }); + + it('should make parseRepoUrl available when requested', async () => { + const parseRepoUrl = jest.fn(() => ({ + repo: 'my-repo', + owner: 'my-owner', + host: 'my-host.com', + })); + const renderWith = await SecureTemplater.loadRenderer({ parseRepoUrl }); + const renderWithout = await SecureTemplater.loadRenderer(); + + const ctx = { + repoUrl: 'https://my-host.com/my-owner/my-repo', + }; + + expect(renderWith('${{ repoUrl | parseRepoUrl | dump }}', ctx)).toBe( + JSON.stringify({ + repo: 'my-repo', + owner: 'my-owner', + host: 'my-host.com', + }), + ); + expect(renderWith('${{ repoUrl | projectSlug }}', ctx)).toBe( + 'my-owner/my-repo', + ); + expect(() => + renderWithout('${{ repoUrl | parseRepoUrl | dump }}', ctx), + ).toThrow(/Error: filter not found: parseRepoUrl/); + expect(() => renderWithout('${{ repoUrl | projectSlug }}', ctx)).toThrow( + /Error: filter not found: projectSlug/, + ); + + expect(parseRepoUrl.mock.calls).toEqual([ + ['https://my-host.com/my-owner/my-repo'], + ['https://my-host.com/my-owner/my-repo'], + ]); + }); + + it('should not allow helpers to be rewritten', async () => { + const render = await SecureTemplater.loadRenderer({ + parseRepoUrl: () => ({ + repo: 'my-repo', + owner: 'my-owner', + host: 'my-host.com', + }), + }); + + const ctx = { + repoUrl: 'https://my-host.com/my-owner/my-repo', + }; + expect( + render( + '${{ ({}).constructor.constructor("parseRepoUrl = () => JSON.stringify(`inject`)")() }}', + ctx, + ), + ).toBe(''); + + expect(render('${{ repoUrl | parseRepoUrl | dump }}', ctx)).toBe( + JSON.stringify({ + repo: 'my-repo', + owner: 'my-owner', + host: 'my-host.com', + }), + ); + }); + + it('allows pollution during a single template execution', async () => { + const render = await SecureTemplater.loadRenderer(); + + const ctx = { + x: 'foo', + }; + expect(render('${{ x }}', ctx)).toBe('foo'); + expect( + render( + '${{ ({}).constructor.constructor("Array.prototype.forEach = () => {}")() }}', + ctx, + ), + ).toBe(''); + expect(() => render('${{ x }}', ctx)).toThrow(); + }); +}); diff --git a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts new file mode 100644 index 0000000000..54c9166020 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts @@ -0,0 +1,141 @@ +/* + * 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 { VM } from 'vm2'; +import { resolvePackagePath } from '@backstage/backend-common'; +import fs from 'fs-extra'; +import { RepoSpec } from '../../scaffolder/actions/builtin/publish/util'; + +const mkScript = (nunjucksSource: string) => ` +const { render, renderCompat } = (() => { + const module = {}; + const process = { env: {} }; + const require = (pkg) => { if (pkg === 'events') { return function (){}; }}; + + ${nunjucksSource} + + const env = module.exports.configure({ + autoescape: false, + tags: { + variableStart: '\${{', + variableEnd: '}}', + }, + }); + + const compatEnv = module.exports.configure({ + autoescape: false, + tags: { + variableStart: '{{', + variableEnd: '}}', + }, + }); + compatEnv.addFilter('jsonify', compatEnv.getFilter('dump')); + + if (typeof parseRepoUrl !== 'undefined') { + const safeHelperRef = parseRepoUrl; + + env.addFilter('parseRepoUrl', repoUrl => { + return JSON.parse(safeHelperRef(repoUrl)) + }); + env.addFilter('projectSlug', repoUrl => { + const { owner, repo } = JSON.parse(safeHelperRef(repoUrl)); + return owner + '/' + repo; + }); + } + + let uninstallCompat = undefined; + + function render(str, values) { + try { + if (uninstallCompat) { + uninstallCompat(); + uninstallCompat = undefined; + } + return env.renderString(str, JSON.parse(values)); + } catch (error) { + // Make sure errors don't leak anything + throw new Error(String(error.message)); + } + } + + function renderCompat(str, values) { + try { + if (!uninstallCompat) { + uninstallCompat = module.exports.installJinjaCompat(); + } + return compatEnv.renderString(str, JSON.parse(values)); + } catch (error) { + // Make sure errors don't leak anything + throw new Error(String(error.message)); + } + } + + return { render, renderCompat }; +})(); +`; + +export interface SecureTemplaterOptions { + /* Optional implementation of the parseRepoUrl filter */ + parseRepoUrl?(repoUrl: string): RepoSpec; + + /* Enables jinja compatibility and the "jsonify" filter */ + cookiecutterCompat?: boolean; +} + +export type SecureTemplateRenderer = ( + template: string, + values: unknown, +) => string; + +export class SecureTemplater { + static async loadRenderer(options: SecureTemplaterOptions = {}) { + const { parseRepoUrl, cookiecutterCompat } = options; + let sandbox = undefined; + + if (parseRepoUrl) { + sandbox = { + parseRepoUrl: (url: string) => JSON.stringify(parseRepoUrl(url)), + }; + } + + const vm = new VM({ sandbox }); + + const nunjucksSource = await fs.readFile( + resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'assets/nunjucks.js.txt', + ), + 'utf-8', + ); + + vm.run(mkScript(nunjucksSource)); + + const render: SecureTemplateRenderer = (template, values) => { + if (!vm) { + throw new Error('SecureTemplater has not been initialized'); + } + vm.setGlobal('templateStr', template); + vm.setGlobal('templateValues', JSON.stringify(values)); + + if (cookiecutterCompat) { + return vm.run(`renderCompat(templateStr, templateValues)`); + } + + return vm.run(`render(templateStr, templateValues)`); + }; + return render; + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index 45481b47e6..b813244020 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -15,10 +15,10 @@ */ import fs from 'fs-extra'; -import { resolve as resolvePath } from 'path'; import { createTemplateAction } from '../../createTemplateAction'; import * as yaml from 'yaml'; import { Entity } from '@backstage/catalog-model'; +import { resolveSafeChildPath } from '@backstage/backend-common'; export function createCatalogWriteAction() { return createTemplateAction<{ name?: string; entity: Entity }>({ @@ -42,7 +42,7 @@ export function createCatalogWriteAction() { const { entity } = ctx.input; await fs.writeFile( - resolvePath(ctx.workspacePath, 'catalog-info.yaml'), + resolveSafeChildPath(ctx.workspacePath, 'catalog-info.yaml'), yaml.stringify(entity), ); }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 9bf9b7fb90..36ea842a66 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -15,7 +15,7 @@ */ import { readdir, stat } from 'fs-extra'; -import { relative, resolve } from 'path'; +import { relative, join } from 'path'; import { createTemplateAction } from '../../createTemplateAction'; /** @@ -68,7 +68,7 @@ export async function recursiveReadDir(dir: string): Promise { const subdirs = await readdir(dir); const files = await Promise.all( subdirs.map(async subdir => { - const res = resolve(dir, subdir); + const res = join(dir, subdir); return (await stat(res)).isDirectory() ? recursiveReadDir(res) : [res]; }), ); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts index 3946110d42..b56bab0d53 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts @@ -19,7 +19,7 @@ import { JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; -import * as path from 'path'; +import path from 'path'; export async function fetchContents({ reader, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 755b435149..cf1662307c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -33,6 +33,17 @@ jest.mock('./helpers', () => ({ fetchContents: jest.fn(), })); +const realFiles = Object.fromEntries( + [ + require.resolve('vm2/lib/fixasync'), + resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'assets', + 'nunjucks.js.txt', + ), + ].map(k => [k, mockFs.load(k)]), +); + const aBinaryFile = fs.readFileSync( resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -76,7 +87,9 @@ describe('fetch:template', () => { }); beforeEach(() => { - mockFs(); + mockFs({ + ...realFiles, + }); action = createFetchTemplateAction({ reader: Symbol('UrlReader') as unknown as UrlReader, @@ -150,6 +163,7 @@ describe('fetch:template', () => { mockFetchContents.mockImplementation(({ outputPath }) => { mockFs({ + ...realFiles, [outputPath]: { 'an-executable.sh': mockFs.file({ content: '#!/usr/bin/env bash', @@ -259,6 +273,7 @@ describe('fetch:template', () => { mockFetchContents.mockImplementation(({ outputPath }) => { mockFs({ + ...realFiles, [outputPath]: { processed: { 'templated-content-${{ values.name }}.txt': @@ -312,6 +327,7 @@ describe('fetch:template', () => { mockFetchContents.mockImplementation(({ outputPath }) => { mockFs({ + ...realFiles, [outputPath]: { '{{ cookiecutter.name }}.txt': 'static content', subdir: { @@ -366,6 +382,7 @@ describe('fetch:template', () => { mockFetchContents.mockImplementation(({ outputPath }) => { mockFs({ + ...realFiles, [outputPath]: { 'empty-dir-${{ values.count }}': {}, 'static.txt': 'static content', @@ -447,6 +464,7 @@ describe('fetch:template', () => { mockFetchContents.mockImplementation(({ outputPath }) => { mockFs({ + ...realFiles, [outputPath]: { '${{ values.name }}.njk': '${{ values.name }}: ${{ values.count }}', '${{ values.name }}.txt.jinja2': diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index 47bfcccfdf..2e25547e61 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -14,29 +14,16 @@ * limitations under the License. */ -import { resolve as resolvePath, extname } from 'path'; +import { extname } from 'path'; import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from './helpers'; import { createTemplateAction } from '../../createTemplateAction'; import globby from 'globby'; -import nunjucks from 'nunjucks'; import fs from 'fs-extra'; import { isBinaryFile } from 'isbinaryfile'; - -/* - * Maximise compatibility with Jinja (and therefore cookiecutter) - * using nunjucks jinja compat mode. Since this method mutates - * the global nunjucks instance, we can't enable this per-template, - * or only for templates with cookiecutter compat enabled, so the - * next best option is to explicitly enable it globally and allow - * folks to rely on jinja compatibility behaviour in fetch:template - * templates if they wish. - * - * cf. https://mozilla.github.io/nunjucks/api.html#installjinjacompat - */ -nunjucks.installJinjaCompat(); +import { SecureTemplater } from '../../../../lib/templating/SecureTemplater'; type CookieCompatInput = { copyWithoutRender?: string[]; @@ -114,7 +101,7 @@ export function createFetchTemplateAction(options: { ctx.logger.info('Fetching template content from remote URL'); const workDir = await ctx.createTemporaryDirectory(); - const templateDir = resolvePath(workDir, 'template'); + const templateDir = resolveSafeChildPath(workDir, 'template'); const targetPath = ctx.input.targetPath ?? './'; const outputDir = resolveSafeChildPath(ctx.workspacePath, targetPath); @@ -179,36 +166,6 @@ export function createFetchTemplateAction(options: { ).flat(), ); - // Create a templater - const templater = nunjucks.configure({ - ...(ctx.input.cookiecutterCompat - ? {} - : { - tags: { - // TODO(mtlewis/orkohunter): Document Why we are changing the literals? Not here, but on scaffolder docs. ADR? - variableStart: '${{', - variableEnd: '}}', - }, - }), - // We don't want this builtin auto-escaping, since uses HTML escape sequences - // like `"` - the correct way to escape strings in our case depends on - // the file type. - autoescape: false, - }); - - if (ctx.input.cookiecutterCompat) { - // The "jsonify" filter built into cookiecutter is common - // in fetch:cookiecutter templates, so when compat mode - // is enabled we alias the "dump" filter from nunjucks as - // jsonify. Dump accepts an optional `spaces` parameter - // which enables indented output, but when this parameter - // is not supplied it works identically to jsonify. - // - // cf. https://cookiecutter.readthedocs.io/en/latest/advanced/template_extensions.html?highlight=jsonify#jsonify-extension - // cf. https://mozilla.github.io/nunjucks/templating.html#dump - templater.addFilter('jsonify', templater.getFilter('dump')); - } - // Cookiecutter prefixes all parameters in templates with // `cookiecutter.`. To replicate this, we wrap our parameters // in an object with a `cookiecutter` property when compat @@ -223,6 +180,10 @@ export function createFetchTemplateAction(options: { ctx.input.values, ); + const renderTemplate = await SecureTemplater.loadRenderer({ + cookiecutterCompat: ctx.input.cookiecutterCompat, + }); + for (const location of allEntriesInTemplate) { let renderFilename: boolean; let renderContents: boolean; @@ -238,9 +199,9 @@ export function createFetchTemplateAction(options: { renderFilename = renderContents = !nonTemplatedEntries.has(location); } if (renderFilename) { - localOutputPath = templater.renderString(localOutputPath, context); + localOutputPath = renderTemplate(localOutputPath, context); } - const outputPath = resolvePath(outputDir, localOutputPath); + const outputPath = resolveSafeChildPath(outputDir, localOutputPath); // variables have been expanded to make an empty file name // this is due to a conditional like if values.my_condition then file-name.txt else empty string so skip if (outputDir === outputPath) { @@ -259,7 +220,7 @@ export function createFetchTemplateAction(options: { ); await fs.ensureDir(outputPath); } else { - const inputFilePath = resolvePath(templateDir, location); + const inputFilePath = resolveSafeChildPath(templateDir, location); if (await isBinaryFile(inputFilePath)) { ctx.logger.info( @@ -275,7 +236,7 @@ export function createFetchTemplateAction(options: { await fs.outputFile( outputPath, renderContents - ? templater.renderString(inputFileContents, context) + ? renderTemplate(inputFileContents, context) : inputFileContents, { mode: statsObj.mode }, ); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index eff65e329f..90f060fd88 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { spawn } from 'child_process'; +import { SpawnOptionsWithoutStdio, spawn } from 'child_process'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { Git } from '@backstage/backend-common'; @@ -22,18 +22,27 @@ import { Octokit } from '@octokit/rest'; import { assertError } from '@backstage/errors'; export type RunCommandOptions = { + /** command to run */ command: string; + /** arguments to pass the command */ args: string[]; + /** options to pass to spawn */ + options?: SpawnOptionsWithoutStdio; + /** stream to capture stdout and stderr output */ logStream?: Writable; }; +/** + * Run a command in a sub-process, normally a shell command. + */ export const runCommand = async ({ command, args, logStream = new PassThrough(), + options, }: RunCommandOptions) => { await new Promise((resolve, reject) => { - const process = spawn(command, args); + const process = spawn(command, args, options); process.stdout.on('data', stream => { logStream.write(stream); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index daa6bc249b..fbfe87b076 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -19,7 +19,7 @@ import { BitbucketIntegrationConfig, ScmIntegrationRegistry, } from '@backstage/integration'; -import fetch from 'cross-fetch'; +import fetch, { Response, RequestInit } from 'node-fetch'; import { initRepoAndPush } from '../helpers'; import { createTemplateAction } from '../../createTemplateAction'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 0cceb47d71..3a9dd01686 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -15,7 +15,6 @@ */ import fs from 'fs-extra'; -import path from 'path'; import { parseRepoUrl, isExecutable } from './util'; import { @@ -197,7 +196,7 @@ export const createPublishGithubPullRequestAction = ({ const fileContents = await Promise.all( localFilePaths.map(filePath => { - const absPath = path.resolve(fileRoot, filePath); + const absPath = resolveSafeChildPath(fileRoot, filePath); const base64EncodedContent = fs .readFileSync(absPath) .toString('base64'); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts similarity index 92% rename from plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 1f7877791c..b933990953 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -17,13 +17,24 @@ import mockFs from 'mock-fs'; import * as winston from 'winston'; -import { getVoidLogger } from '@backstage/backend-common'; +import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { TaskContext, TaskSpec } from './types'; +const realFiles = Object.fromEntries( + [ + require.resolve('vm2/lib/fixasync'), + resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'assets', + 'nunjucks.js.txt', + ), + ].map(k => [k, mockFs.load(k)]), +); + describe('DefaultWorkflowRunner', () => { const logger = getVoidLogger(); let actionRegistry = new TemplateActionRegistry(); @@ -50,6 +61,7 @@ describe('DefaultWorkflowRunner', () => { winston.format.simple(); // put logform the require cache before mocking fs mockFs({ '/tmp': mockFs.directory(), + ...realFiles, }); jest.resetAllMocks(); @@ -270,6 +282,31 @@ describe('DefaultWorkflowRunner', () => { ); }); + it('should not try and parse something that is not parsable', async () => { + jest.spyOn(logger, 'error'); + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-mock-action', + input: { + foo: 'bob', + }, + }, + ], + output: {}, + parameters: { + input: 'BACKSTAGE', + }, + }); + + await runner.execute(task); + + expect(logger.error).not.toHaveBeenCalled(); + }); + it('should keep the original types for the input and not parse things that arent meant to be parsed', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index c66d967103..a3e30c65fe 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ScmIntegrations } from '@backstage/integration'; import { TaskContext, @@ -23,9 +24,9 @@ import { WorkflowRunner, } from './types'; import * as winston from 'winston'; -import nunjucks from 'nunjucks'; import fs from 'fs-extra'; import path from 'path'; +import nunjucks from 'nunjucks'; import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { PassThrough } from 'stream'; @@ -33,6 +34,10 @@ import { isTruthy } from './helper'; import { validate as validateJsonSchema } from 'jsonschema'; import { parseRepoUrl } from '../actions/builtin/publish/util'; import { TemplateActionRegistry } from '../actions'; +import { + SecureTemplater, + SecureTemplateRenderer, +} from '../../lib/templating/SecureTemplater'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -84,43 +89,43 @@ const createStepLogger = ({ }; export class NunjucksWorkflowRunner implements WorkflowRunner { - private readonly nunjucks: nunjucks.Environment; - - private readonly nunjucksOptions: nunjucks.ConfigureOptions = { - autoescape: false, - tags: { - variableStart: '${{', - variableEnd: '}}', - }, - }; - - constructor(private readonly options: NunjucksWorkflowRunnerOptions) { - this.nunjucks = nunjucks.configure(this.nunjucksOptions); - - // TODO(blam): let's work out how we can deprecate these. - // We shouldn't really need to be exposing these now we can deal with - // objects in the params block. - // Maybe we can expose a new RepoUrlPicker with secrets for V3 that provides an object already. - this.nunjucks.addFilter('parseRepoUrl', repoUrl => { - return parseRepoUrl(repoUrl, this.options.integrations); - }); - - this.nunjucks.addFilter('projectSlug', repoUrl => { - const { owner, repo } = parseRepoUrl(repoUrl, this.options.integrations); - return `${owner}/${repo}`; - }); - } + constructor(private readonly options: NunjucksWorkflowRunnerOptions) {} private isSingleTemplateString(input: string) { - const { parser, nodes } = require('nunjucks'); - const parsed = parser.parse(input, {}, this.nunjucksOptions); + const { parser, nodes } = nunjucks as unknown as { + parser: { + parse( + template: string, + ctx: object, + options: nunjucks.ConfigureOptions, + ): { children: { children?: unknown[] }[] }; + }; + nodes: { TemplateData: Function }; + }; + + const parsed = parser.parse( + input, + {}, + { + autoescape: false, + tags: { + variableStart: '${{', + variableEnd: '}}', + }, + }, + ); + return ( parsed.children.length === 1 && - !(parsed.children[0] instanceof nodes.TemplateData) + !(parsed.children[0]?.children?.[0] instanceof nodes.TemplateData) ); } - private render(input: T, context: TemplateContext): T { + private render( + input: T, + context: TemplateContext, + renderTemplate: SecureTemplateRenderer, + ): T { return JSON.parse(JSON.stringify(input), (_key, value) => { try { if (typeof value === 'string') { @@ -133,10 +138,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { ); // Run the templating - const templated = this.nunjucks.renderString( - wrappedDumped, - context, - ); + const templated = renderTemplate(wrappedDumped, context); // If there's an empty string returned, then it's undefined if (templated === '') { @@ -153,7 +155,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } // Fallback to default behaviour - const templated = this.nunjucks.renderString(value, context); + const templated = renderTemplate(value, context); if (templated === '') { return undefined; @@ -178,6 +180,18 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { this.options.workingDirectory, await task.getWorkspaceName(), ); + + const { integrations } = this.options; + const renderTemplate = await SecureTemplater.loadRenderer({ + // TODO(blam): let's work out how we can deprecate this. + // We shouldn't really need to be exposing these now we can deal with + // objects in the params block. + // Maybe we can expose a new RepoUrlPicker with secrets for V3 that provides an object already. + parseRepoUrl(url: string) { + return parseRepoUrl(url, integrations); + }, + }); + try { await fs.ensureDir(workspacePath); await task.emitLog( @@ -192,7 +206,11 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { for (const step of task.spec.steps) { try { if (step.if) { - const ifResult = await this.render(step.if, context); + const ifResult = await this.render( + step.if, + context, + renderTemplate, + ); if (!isTruthy(ifResult)) { await task.emitLog( `Skipping step ${step.id} because it's if condition was false`, @@ -210,7 +228,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const action = this.options.actionRegistry.get(step.action); const { taskLogger, streamLogger } = createStepLogger({ task, step }); - const input = (step.input && this.render(step.input, context)) ?? {}; + const input = + (step.input && this.render(step.input, context, renderTemplate)) ?? + {}; if (action.schema?.input) { const validateResult = validateJsonSchema( @@ -273,7 +293,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { } } - const output = this.render(task.spec.output, context); + const output = this.render(task.spec.output, context, renderTemplate); return { output }; } finally { diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 8c43d8d010..e809ae6c71 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder +## 0.11.12 + +### Patch Changes + +- 2d7d165737: Bump `react-jsonschema-form` +- 9f21236a29: Fixed a missing `await` when throwing server side errors +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.11.11 ### Patch Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 0ac6910264..73fd8fae72 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -24,6 +24,7 @@ import { JSONSchema } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; import { default as React_2 } from 'react'; +import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; @@ -213,12 +214,20 @@ export const ScaffolderFieldExtensions: React_2.ComponentType; // @public (undocumented) export const ScaffolderPage: ({ TemplateCardComponent, + groups, }: { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2; }> | undefined; + groups?: + | { + title?: string | undefined; + titleComponent?: ReactNode; + filter: (entity: Entity) => boolean; + }[] + | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "scaffolderPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -240,7 +249,8 @@ export { scaffolderPlugin }; // @public (undocumented) export const TemplateList: ({ TemplateCardComponent, -}: TemplateListProps) => JSX.Element; + group, +}: TemplateListProps) => JSX.Element | null; // Warning: (ae-missing-release-tag) "TemplateListProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -251,6 +261,11 @@ export type TemplateListProps = { template: TemplateEntityV1beta2; }> | undefined; + group?: { + title?: string; + titleComponent?: React_2.ReactNode; + filter: (entity: Entity) => boolean; + }; }; // Warning: (ae-missing-release-tag) "TemplateTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index ce185403bb..2cd341d5b9 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.11.11", + "version": "0.11.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/errors": "^0.1.4", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", + "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.9", "@backstage/integration-react": "^0.1.14", "@backstage/plugin-catalog-react": "^0.6.4", @@ -66,10 +66,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index 77c8240974..ce0354b817 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -17,8 +17,8 @@ import React from 'react'; import { ScaffolderApi, scaffolderApiRef } from '../../api'; import { ActionsPage } from './ActionsPage'; import { rootRouteRef } from '../../routes'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { ApiProvider } from '@backstage/core-app-api'; const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), @@ -29,7 +29,7 @@ const scaffolderApiMock: jest.Mocked = { listActions: jest.fn(), }; -const apis = ApiRegistry.from([[scaffolderApiRef, scaffolderApiMock]]); +const apis = TestApiRegistry.from([scaffolderApiRef, scaffolderApiMock]); describe('TemplatePage', () => { beforeEach(() => jest.resetAllMocks()); diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 9e4736ab9f..e667c676f2 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -16,7 +16,7 @@ import React, { ComponentType } from 'react'; import { Routes, Route, useOutlet } from 'react-router'; -import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; @@ -34,9 +34,14 @@ type RouterProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; + groups?: Array<{ + title?: string; + titleComponent?: React.ReactNode; + filter: (entity: Entity) => boolean; + }>; }; -export const Router = ({ TemplateCardComponent }: RouterProps) => { +export const Router = ({ TemplateCardComponent, groups }: RouterProps) => { const outlet = useOutlet(); const customFieldExtensions = useElementFilter(outlet, elements => @@ -64,7 +69,10 @@ export const Router = ({ TemplateCardComponent }: RouterProps) => { + } /> | undefined; + groups?: Array<{ + title?: string; + titleComponent?: React.ReactNode; + filter: (entity: Entity) => boolean; + }>; }; export const ScaffolderPageContents = ({ TemplateCardComponent, + groups, }: ScaffolderPageProps) => { const styles = useStyles(); - const registerComponentLink = useRouteRef(registerComponentRouteRef); + const otherTemplatesGroup = { + title: groups ? 'Other Templates' : 'Templates', + filter: (entity: Entity) => { + const filtered = (groups ?? []).map(group => group.filter(entity)); + return !filtered.some(result => result === true); + }, + }; return ( @@ -96,7 +108,17 @@ export const ScaffolderPageContents = ({
- + {groups && + groups.map(group => ( + + ))} +
@@ -106,8 +128,12 @@ export const ScaffolderPageContents = ({ export const ScaffolderPage = ({ TemplateCardComponent, + groups, }: ScaffolderPageProps) => ( - + ); diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index dcf708db10..9b66a0a9b6 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -16,10 +16,13 @@ import React, { ComponentType } from 'react'; import { + Entity, stringifyEntityRef, TemplateEntityV1beta2, } from '@backstage/catalog-model'; import { + Content, + ContentHeader, ItemCardGrid, Progress, WarningPanel, @@ -32,11 +35,31 @@ export type TemplateListProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; + group?: { + title?: string; + titleComponent?: React.ReactNode; + filter: (entity: Entity) => boolean; + }; }; -export const TemplateList = ({ TemplateCardComponent }: TemplateListProps) => { +export const TemplateList = ({ + TemplateCardComponent, + group, +}: TemplateListProps) => { const { loading, error, entities } = useEntityListProvider(); const Card = TemplateCardComponent || TemplateCard; + const maybeFilteredEntities = group + ? entities.filter(e => group.filter(e)) + : entities; + const title = group ? ( + group.titleComponent || + ) : ( + + ); + + if (group && maybeFilteredEntities.length === 0) { + return null; + } return ( <> {loading && } @@ -57,16 +80,19 @@ export const TemplateList = ({ TemplateCardComponent }: TemplateListProps) => { )} - - {entities && - entities?.length > 0 && - entities.map(template => ( - - ))} - + + {title} + + {maybeFilteredEntities && + maybeFilteredEntities?.length > 0 && + maybeFilteredEntities.map((template: Entity) => ( + + ))} + + ); }; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index e42bcbdbdc..60e9ee8840 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { renderInTestApp, renderWithEffects } from '@backstage/test-utils'; +import { + renderInTestApp, + renderWithEffects, + TestApiRegistry, +} from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { fireEvent, within } from '@testing-library/react'; @@ -24,7 +28,7 @@ import { ScaffolderApi, scaffolderApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; import { TemplatePage } from './TemplatePage'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; jest.mock('react-router-dom', () => { @@ -47,10 +51,10 @@ const scaffolderApiMock: jest.Mocked = { const errorApiMock = { post: jest.fn(), error$: jest.fn() }; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [errorApiRef, errorApiMock], -]); +); describe('TemplatePage', () => { beforeEach(() => jest.resetAllMocks()); diff --git a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx index 3d0267ff1f..c70f251eac 100644 --- a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx +++ b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx @@ -26,8 +26,8 @@ import { MockEntityListContextProvider, } from '@backstage/plugin-catalog-react'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { renderWithEffects } from '@backstage/test-utils'; +import { ApiProvider } from '@backstage/core-app-api'; +import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; const entities: Entity[] = [ { @@ -62,7 +62,7 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [ catalogApiRef, { @@ -77,7 +77,7 @@ const apis = ApiRegistry.from([ post: jest.fn(), } as unknown as AlertApi, ], -]); +); describe('', () => { it('renders available entity types', async () => { diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index f293969dff..b7e72e3583 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -16,12 +16,11 @@ import { Entity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { FieldProps } from '@rjsf/core'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { EntityPicker } from './EntityPicker'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'backstage.io/v1beta1', @@ -53,14 +52,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); entities = [ makeEntity('Group', 'default', 'team-a'), makeEntity('Group', 'default', 'squad-b'), ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx index 18d6fd7e5c..31992fbc8a 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { FieldProps } from '@rjsf/core'; import React from 'react'; import { OwnerPicker } from './OwnerPicker'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'backstage.io/v1beta1', @@ -50,14 +49,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); entities = [ makeEntity('Group', 'default', 'team-a'), makeEntity('Group', 'default', 'squad-b'), ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index af8fcfa79a..bf0c2df174 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-search-backend +## 0.2.7 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/backend-common@0.9.11 + ## 0.2.6 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 2df51e1bb4..3767944a25 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/search-common": "^0.2.0", "@backstage/plugin-search-backend-node": "^0.4.2", "@types/express": "^4.17.6", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index c211c48e53..7dba0a8e21 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -84,6 +84,7 @@ export const SearchBar: ({ className, debounceTime, placeholder, + clearButton, }: Props) => JSX.Element; // Warning: (ae-missing-release-tag) "SearchBarNext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -94,11 +95,13 @@ export const SearchBarNext: ({ className, debounceTime, placeholder, + clearButton, }: { autoFocus?: boolean | undefined; className?: string | undefined; debounceTime?: number | undefined; placeholder?: string | undefined; + clearButton?: boolean | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "SearchContextProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/search/package.json b/plugins/search/package.json index aaddf130bf..a032f5ef26 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -32,8 +32,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.4", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/search-common": "^0.2.1", @@ -51,10 +51,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx index 72b8429026..d00f391bc7 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { Paper, Grid } from '@material-ui/core'; +import { Paper, Grid, makeStyles } from '@material-ui/core'; import { SearchBar, SearchContext } from '../index'; import { MemoryRouter } from 'react-router'; @@ -81,3 +81,48 @@ export const Focused = () => { ); }; + +export const WithoutClearButton = () => { + return ( + + {/* @ts-ignore (defaultValue requires more than what is used here) */} + + + + + + + + + + + ); +}; + +const useStyles = makeStyles({ + search: { + display: 'flex', + justifyContent: 'space-between', + padding: '8px 0', + borderRadius: '50px', + margin: 'auto', + }, +}); + +export const CustomStyles = () => { + const classes = useStyles(); + return ( + + {/* @ts-ignore (defaultValue requires more than what is used here) */} + + + + + + + + + + + ); +}; diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx index 3a11daa9fa..4ac4c95224 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx @@ -21,12 +21,9 @@ import { SearchContextProvider } from '../SearchContext'; import { SearchBar } from './SearchBar'; import { configApiRef } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { searchApiRef } from '../../apis'; +import { TestApiRegistry } from '@backstage/test-utils'; jest.mock('@backstage/core-plugin-api', () => ({ ...jest.requireActual('@backstage/core-plugin-api'), @@ -42,10 +39,10 @@ describe('SearchBar', () => { const query = jest.fn().mockResolvedValue({}); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [configApiRef, new ConfigReader({ app: { title: 'Mock title' } })], [searchApiRef, { query }], - ]); + ); const name = 'Search'; const term = 'term'; @@ -152,6 +149,20 @@ describe('SearchBar', () => { ); }); + it('Should not show clear button', async () => { + render( + + + + + , + ); + + expect( + screen.queryByRole('button', { name: 'Clear' }), + ).not.toBeInTheDocument(); + }); + it('Adheres to provided debounceTime', async () => { jest.useFakeTimers(); diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index 4ab680d8fa..693cf98adf 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -31,6 +31,7 @@ type PresenterProps = { className?: string; placeholder?: string; autoFocus?: boolean; + clearButton?: boolean; }; export const SearchBarBase = ({ @@ -40,6 +41,7 @@ export const SearchBarBase = ({ onSubmit, className, placeholder: overridePlaceholder, + clearButton = true, }: PresenterProps) => { const configApi = useApi(configApiRef); @@ -79,11 +81,13 @@ export const SearchBarBase = ({ } endAdornment={ - - - - - + clearButton && ( + + + + + + ) } {...(className && { className })} {...(onSubmit && { onKeyDown })} @@ -96,6 +100,7 @@ type Props = { className?: string; debounceTime?: number; placeholder?: string; + clearButton?: boolean; }; export const SearchBar = ({ @@ -103,6 +108,7 @@ export const SearchBar = ({ className, debounceTime = 0, placeholder, + clearButton = true, }: Props) => { const { term, setTerm } = useSearch(); const [value, setValue] = useState(term); @@ -129,6 +135,7 @@ export const SearchBar = ({ onChange={handleQuery} onClear={handleClear} placeholder={placeholder} + clearButton={clearButton} /> ); }; diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index d7f873abf3..ff9622dfb9 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -15,14 +15,10 @@ */ import React from 'react'; import { screen } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { rootRouteRef } from '../../plugin'; import { searchApiRef } from '../../apis'; @@ -38,10 +34,10 @@ jest.mock('../SearchContext', () => ({ describe('SearchModal', () => { const query = jest.fn().mockResolvedValue({}); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [configApiRef, new ConfigReader({ app: { title: 'Mock app' } })], [searchApiRef, { query }], - ]); + ); const toggleModal = jest.fn(); diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index a04043c789..2ea32c4f22 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -48,10 +48,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index f0139d5728..0569585733 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", @@ -38,10 +38,10 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx index 692040027c..8cee9a6622 100644 --- a/plugins/shortcuts/src/Shortcuts.test.tsx +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -15,25 +15,31 @@ */ import React from 'react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import { screen, waitFor } from '@testing-library/react'; import { Shortcuts } from './Shortcuts'; import { LocalStoredShortcuts, shortcutsApiRef } from './api'; import { SidebarContext } from '@backstage/core-components'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; - -const apis = ApiRegistry.from([ - [shortcutsApiRef, new LocalStoredShortcuts(MockStorageApi.create())], -]); describe('Shortcuts', () => { it('displays an add button', async () => { await renderInTestApp( {} }}> - + - + , ); await waitFor(() => !screen.queryByTestId('progress')); diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index f92c21ad7e..2c72306efc 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -34,8 +34,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -49,16 +49,15 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 10eb807ba1..ac24c463c6 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -47,10 +47,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index 63d617aff2..eb5c99d4e7 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { act, fireEvent, render, waitFor } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef, SplunkOnCallClient, @@ -37,13 +37,8 @@ import { alertApiRef, ConfigApi, configApiRef, - createApiRef, } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; const mockSplunkOnCallApi: Partial = { getUsers: async () => [], @@ -59,17 +54,11 @@ const configApi: ConfigApi = new ConfigReader({ }, }); -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [splunkOnCallApiRef, mockSplunkOnCallApi], [configApiRef, configApi], - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], -]); + [alertApiRef, {}], +); const mockEntity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx index 1319493742..046c6a37f9 100644 --- a/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx +++ b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { EscalationPolicy } from './EscalationPolicy'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef } from '../../api'; import { MOCKED_ON_CALL, MOCKED_USER } from '../../api/mocks'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockSplunkOnCallApi = { - getOnCallUsers: () => [], + getOnCallUsers: jest.fn(), }; -const apis = ApiRegistry.from([[splunkOnCallApiRef, mockSplunkOnCallApi]]); +const apis = TestApiRegistry.from([splunkOnCallApiRef, mockSplunkOnCallApi]); describe('Escalation', () => { it('Handles an empty response', async () => { diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx index df4254d3ad..181c2fda77 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx @@ -16,43 +16,39 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { Incidents } from './Incidents'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef } from '../../api'; import { MOCK_TEAM, MOCK_INCIDENT } from '../../api/mocks'; import { alertApiRef, - createApiRef, IdentityApi, identityApiRef, } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockIdentityApi: Partial = { getUserId: () => 'test', }; const mockSplunkOnCallApi = { - getIncidents: () => [], - getTeams: () => [], + getIncidents: jest.fn(), + getTeams: jest.fn(), }; -const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], +const apis = TestApiRegistry.from( + [alertApiRef, {}], [identityApiRef, mockIdentityApi], [splunkOnCallApiRef, mockSplunkOnCallApi], -]); +); describe('Incidents', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('Renders an empty state when there are no incidents', async () => { - mockSplunkOnCallApi.getTeams = jest - .fn() - .mockImplementationOnce(async () => [MOCK_TEAM]); + mockSplunkOnCallApi.getIncidents.mockResolvedValue([]); + mockSplunkOnCallApi.getTeams.mockResolvedValue([MOCK_TEAM]); const { getByText, queryByTestId } = render( wrapInTestApp( @@ -69,13 +65,9 @@ describe('Incidents', () => { }); it('Renders all incidents', async () => { - mockSplunkOnCallApi.getIncidents = jest - .fn() - .mockImplementationOnce(async () => [MOCK_INCIDENT]); + mockSplunkOnCallApi.getIncidents.mockResolvedValue([MOCK_INCIDENT]); + mockSplunkOnCallApi.getTeams.mockResolvedValue([MOCK_TEAM]); - mockSplunkOnCallApi.getTeams = jest - .fn() - .mockImplementationOnce(async () => [MOCK_TEAM]); const { getByText, getByTitle, @@ -108,9 +100,10 @@ describe('Incidents', () => { }); it('Handle errors', async () => { - mockSplunkOnCallApi.getIncidents = jest - .fn() - .mockRejectedValueOnce(new Error('Error occurred')); + mockSplunkOnCallApi.getIncidents.mockRejectedValueOnce( + new Error('Error occurred'), + ); + mockSplunkOnCallApi.getTeams.mockResolvedValue([]); const { getByText, queryByTestId } = render( wrapInTestApp( diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx index 94fd40bfe5..0b64a137af 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -15,12 +15,12 @@ */ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef } from '../../api'; import { TriggerDialog } from './TriggerDialog'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; -import { alertApiRef, createApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { alertApiRef } from '@backstage/core-plugin-api'; describe('TriggerDialog', () => { const mockTriggerAlarmFn = jest.fn(); @@ -28,16 +28,10 @@ describe('TriggerDialog', () => { incidentAction: mockTriggerAlarmFn, }; - const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], + const apis = TestApiRegistry.from( + [alertApiRef, {}], [splunkOnCallApiRef, mockSplunkOnCallApi], - ]); + ); it('open the dialog and trigger an alarm', async () => { const { getByText, getByRole, getByTestId } = render( diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md new file mode 100644 index 0000000000..92bd7ebf33 --- /dev/null +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -0,0 +1,10 @@ +# @backstage/plugin-tech-insights-backend-module-jsonfc + +## 0.1.1 + +### Patch Changes + +- 2017de90da: Update README docs to use correct function/parameter names +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 5504cb2d81..c7086cf98c 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.5", "@backstage/plugin-tech-insights-common": "^0.1.0", "@backstage/plugin-tech-insights-node": "^0.1.0", "ajv": "^7.0.3", @@ -43,7 +43,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/node-cron": "^2.0.4" }, "files": [ diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index abb8eaac6f..0982a3cdc8 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-tech-insights-backend +## 0.1.2 + +### Patch Changes + +- 2017de90da: Update README docs to use correct function/parameter names +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + ## 0.1.1 ### Patch Changes diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 5bc616db71..5852702cd6 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,15 +31,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.8", - "@backstage/errors": "^0.1.1", + "@backstage/errors": "^0.1.5", "@backstage/plugin-tech-insights-common": "^0.1.0", "@backstage/plugin-tech-insights-node": "^0.1.0", "@types/express": "^4.17.6", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^0.95.1", @@ -53,7 +52,7 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.9", - "@backstage/cli": "^0.9.0", + "@backstage/cli": "^0.9.1", "@types/supertest": "^2.0.8", "@types/node-cron": "^3.0.0", "@types/semver": "^7.3.8", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 99fce10345..5b11fbbe7c 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -31,8 +31,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,10 +45,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/src/components/RadarComponent.test.tsx b/plugins/tech-radar/src/components/RadarComponent.test.tsx index cce411e897..2b237c8d83 100644 --- a/plugins/tech-radar/src/components/RadarComponent.test.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.test.tsx @@ -19,13 +19,12 @@ import { render, waitForElement } from '@testing-library/react'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { act } from 'react-dom/test-utils'; -import { withLogCollector } from '@backstage/test-utils'; +import { TestApiProvider, withLogCollector } from '@backstage/test-utils'; import GetBBoxPolyfill from '../utils/polyfills/getBBox'; import { RadarComponent } from './RadarComponent'; import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; describe('RadarComponent', () => { @@ -55,18 +54,18 @@ describe('RadarComponent', () => { const errorApi = { post: () => {} }; const { getByTestId, queryByTestId } = render( - - + , ); @@ -87,18 +86,18 @@ describe('RadarComponent', () => { const { queryByTestId } = render( - - + , ); @@ -115,13 +114,13 @@ describe('RadarComponent', () => { expect(() => { render( - + - + , ); }).toThrow(); diff --git a/plugins/tech-radar/src/components/RadarPage.test.tsx b/plugins/tech-radar/src/components/RadarPage.test.tsx index 7215fc40df..7f4e69e388 100644 --- a/plugins/tech-radar/src/components/RadarPage.test.tsx +++ b/plugins/tech-radar/src/components/RadarPage.test.tsx @@ -17,6 +17,7 @@ import { MockErrorApi, renderInTestApp, + TestApiProvider, wrapInTestApp, } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; @@ -28,7 +29,6 @@ import GetBBoxPolyfill from '../utils/polyfills/getBBox'; import { RadarPage } from './RadarPage'; import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; describe('RadarPage', () => { @@ -63,9 +63,9 @@ describe('RadarPage', () => { const { getByTestId, queryByTestId } = render( wrapInTestApp( - + - + , ), ); @@ -89,9 +89,9 @@ describe('RadarPage', () => { const { getByText, getByTestId } = await renderInTestApp( - + - + , ); @@ -115,9 +115,9 @@ describe('RadarPage', () => { const { getByTestId } = await renderInTestApp( - + - + , ); @@ -142,14 +142,14 @@ describe('RadarPage', () => { const { queryByTestId } = await renderInTestApp( - - + , ); diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index ad7a8af85e..7c937e0bad 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-techdocs-backend +## 0.10.9 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + - @backstage/techdocs-common@0.10.8 + ## 0.10.8 ### Patch Changes diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index a6094c2bdd..f9be757e6a 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -14,6 +14,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PreparerBuilder } from '@backstage/techdocs-common'; import { PublisherBase } from '@backstage/techdocs-common'; import { TechDocsDocument } from '@backstage/techdocs-common'; +import { TokenManager } from '@backstage/backend-common'; // Warning: (ae-forgotten-export) The symbol "RouterOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -31,6 +32,7 @@ export class DefaultTechDocsCollator implements DocumentCollator { locationTemplate, logger, catalogClient, + tokenManager, parallelismLimit, legacyPathCasing, }: TechDocsCollatorOptions); @@ -60,6 +62,7 @@ export class DefaultTechDocsCollator implements DocumentCollator { export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; logger: Logger_2; + tokenManager: TokenManager; locationTemplate?: string; catalogClient?: CatalogApi; parallelismLimit?: number; diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 332884c694..ffad4bc4c1 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.10.8", + "version": "0.10.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,28 +31,28 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.10", + "@backstage/backend-common": "^0.9.11", "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/errors": "^0.1.4", + "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.9", "@backstage/search-common": "^0.2.1", - "@backstage/techdocs-common": "^0.10.7", + "@backstage/techdocs-common": "^0.10.8", "@types/express": "^4.17.6", - "cross-fetch": "^3.0.6", "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", "knex": "^0.95.1", "lodash": "^4.17.21", + "node-fetch": "^2.6.1", "p-limit": "^3.1.0", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/test-utils": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/test-utils": "^0.1.23", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 21be228636..5f0bed55dd 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -17,6 +17,7 @@ import { PluginEndpointDiscovery, getVoidLogger, + TokenManager, } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; @@ -87,6 +88,7 @@ const expectedEntities: Entity[] = [ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { let mockDiscoveryApi: jest.Mocked; + let mockTokenManager: jest.Mocked; let collator: DefaultTechDocsCollator; const worker = setupServer(); @@ -96,6 +98,10 @@ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), getExternalBaseUrl: jest.fn(), }; + mockTokenManager = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; const mockConfig = new ConfigReader({ techdocs: { legacyUseCaseSensitiveTripletPaths: true, @@ -103,6 +109,7 @@ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { }); collator = DefaultTechDocsCollator.fromConfig(mockConfig, { discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, logger, legacyPathCasing: true, }); @@ -147,6 +154,7 @@ describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { describe('DefaultTechDocsCollator', () => { let mockDiscoveryApi: jest.Mocked; + let mockTokenManager: jest.Mocked; let collator: DefaultTechDocsCollator; const worker = setupServer(); @@ -156,8 +164,13 @@ describe('DefaultTechDocsCollator', () => { getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), getExternalBaseUrl: jest.fn(), }; + mockTokenManager = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; collator = DefaultTechDocsCollator.fromConfig(new ConfigReader({}), { discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, logger, }); @@ -195,6 +208,7 @@ describe('DefaultTechDocsCollator', () => { // Provide an alternate location template. collator = new DefaultTechDocsCollator({ discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, locationTemplate: '/software/:name', logger, }); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index 121ef6e46d..283d0264e4 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -14,10 +14,13 @@ * limitations under the License. */ -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { DocumentCollator } from '@backstage/search-common'; -import fetch from 'cross-fetch'; +import fetch from 'node-fetch'; import unescape from 'lodash/unescape'; import { Logger } from 'winston'; import pLimit from 'p-limit'; @@ -34,6 +37,7 @@ interface MkSearchIndexDoc { export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; logger: Logger; + tokenManager: TokenManager; locationTemplate?: string; catalogClient?: CatalogApi; parallelismLimit?: number; @@ -51,6 +55,7 @@ export class DefaultTechDocsCollator implements DocumentCollator { protected locationTemplate: string; private readonly logger: Logger; private readonly catalogClient: CatalogApi; + private readonly tokenManager: TokenManager; private readonly parallelismLimit: number; private readonly legacyPathCasing: boolean; public readonly type: string = 'techdocs'; @@ -63,6 +68,7 @@ export class DefaultTechDocsCollator implements DocumentCollator { locationTemplate, logger, catalogClient, + tokenManager, parallelismLimit = 10, legacyPathCasing = false, }: TechDocsCollatorOptions) { @@ -74,6 +80,7 @@ export class DefaultTechDocsCollator implements DocumentCollator { catalogClient || new CatalogClient({ discoveryApi: discovery }); this.parallelismLimit = parallelismLimit; this.legacyPathCasing = legacyPathCasing; + this.tokenManager = tokenManager; } static fromConfig(config: Config, options: TechDocsCollatorOptions) { @@ -87,19 +94,23 @@ export class DefaultTechDocsCollator implements DocumentCollator { async execute() { const limit = pLimit(this.parallelismLimit); const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs'); - const entities = await this.catalogClient.getEntities({ - fields: [ - 'kind', - 'namespace', - 'metadata.annotations', - 'metadata.name', - 'metadata.title', - 'metadata.namespace', - 'spec.type', - 'spec.lifecycle', - 'relations', - ], - }); + const { token } = await this.tokenManager.getToken(); + const entities = await this.catalogClient.getEntities( + { + fields: [ + 'kind', + 'namespace', + 'metadata.annotations', + 'metadata.name', + 'metadata.title', + 'metadata.namespace', + 'spec.type', + 'spec.lifecycle', + 'relations', + ], + }, + { token }, + ); const docPromises = entities.items .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref']) .map((entity: Entity) => diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index e560a895b6..d53a2faed4 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -24,7 +24,7 @@ import { PreparerBuilder, PublisherBase, } from '@backstage/techdocs-common'; -import fetch from 'cross-fetch'; +import fetch from 'node-fetch'; import express, { Response } from 'express'; import Router from 'express-promise-router'; import { Knex } from 'knex'; diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 6b8a8f0ff9..2755daea4a 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-techdocs +## 0.12.7 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/core-plugin-api@0.2.1 + - @backstage/core-components@0.7.5 + ## 0.12.6 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 663c83118e..1677d43095 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.12.6", + "version": "0.12.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", - "@backstage/errors": "^0.1.4", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", + "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.9", "@backstage/integration-react": "^0.1.14", "@backstage/plugin-catalog": "^0.7.3", @@ -61,10 +61,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 1d8d7d2b04..8618c4432f 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { ConfigApi, configApiRef, @@ -30,7 +26,11 @@ import { DefaultStarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { rootDocsRouteRef } from '../../routes'; @@ -69,12 +69,12 @@ describe('TechDocs Home', () => { const storageApi = MockStorageApi.create(); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], [storageApiRef, storageApi], [starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi })], - ]); + ); it('should render a TechDocs home page', async () => { await renderInTestApp( diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx index d9d2c6a345..a8fd472f7c 100644 --- a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx @@ -15,16 +15,12 @@ */ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { LegacyTechDocsHome } from './LegacyTechDocsHome'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { rootDocsRouteRef } from '../../routes'; @@ -59,10 +55,10 @@ describe('Legacy TechDocs Home', () => { }, }); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - ]); + ); it('should render a TechDocs home page', async () => { await renderInTestApp( diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx index bcbfdeed86..e9c3a55b15 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx @@ -15,11 +15,11 @@ */ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { TechDocsCustomHome, PanelType } from './TechDocsCustomHome'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { rootDocsRouteRef } from '../../routes'; jest.mock('@backstage/plugin-catalog-react', () => { @@ -47,7 +47,7 @@ const mockCatalogApi = { } as Partial; describe('TechDocsCustomHome', () => { - const apiRegistry = ApiRegistry.with(catalogApiRef, mockCatalogApi); + const apiRegistry = TestApiRegistry.from([catalogApiRef, mockCatalogApi]); it('should render a TechDocs home page', async () => { const tabsConfig = [ diff --git a/plugins/techdocs/src/reader/components/Reader.test.tsx b/plugins/techdocs/src/reader/components/Reader.test.tsx index 8b5dbced3f..6e8609694d 100644 --- a/plugins/techdocs/src/reader/components/Reader.test.tsx +++ b/plugins/techdocs/src/reader/components/Reader.test.tsx @@ -19,12 +19,12 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import React from 'react'; import { TechDocsStorageApi, techdocsStorageApiRef } from '../../api'; import { Reader } from './Reader'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { searchApiRef } from '@backstage/plugin-search'; jest.mock('react-router-dom', () => { @@ -57,11 +57,11 @@ describe('', () => { results: [], }), }; - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [scmIntegrationsApiRef, scmIntegrationsApi], [techdocsStorageApiRef, techdocsStorageApi], [searchApiRef, searchApi], - ]); + ); await act(async () => { const rendered = render( diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx index 6a5af982fd..b297466a62 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx @@ -21,7 +21,7 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { Header } from '@backstage/core-components'; import { techdocsApiRef, @@ -29,7 +29,7 @@ import { techdocsStorageApiRef, TechDocsStorageApi, } from '../../api'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { searchApiRef } from '@backstage/plugin-search'; jest.mock('react-router-dom', () => { @@ -90,12 +90,12 @@ describe('', () => { results: [], }), }; - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [scmIntegrationsApiRef, scmIntegrationsApi], [techdocsApiRef, techdocsApi], [techdocsStorageApiRef, techdocsStorageApi], [searchApiRef, searchApi], - ]); + ); await act(async () => { const rendered = render( @@ -147,12 +147,12 @@ describe('', () => { results: [], }), }; - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [scmIntegrationsApiRef, scmIntegrationsApi], [techdocsApiRef, techdocsApi], [techdocsStorageApiRef, techdocsStorageApi], [searchApiRef, searchApi], - ]); + ); await act(async () => { const rendered = render( diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx index 602a94e758..5b3d714a04 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { searchApiRef } from '@backstage/plugin-search'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { act, fireEvent, @@ -54,7 +54,7 @@ describe('', () => { const querySpy = jest.fn(query); const searchApi = { query: querySpy }; - const apiRegistry = ApiRegistry.from([[searchApiRef, searchApi]]); + const apiRegistry = TestApiRegistry.from([searchApiRef, searchApi]); await act(async () => { const rendered = render( @@ -75,7 +75,7 @@ describe('', () => { const querySpy = jest.fn(query); const searchApi = { query: querySpy }; - const apiRegistry = ApiRegistry.from([[searchApiRef, searchApi]]); + const apiRegistry = TestApiRegistry.from([searchApiRef, searchApi]); await act(async () => { const rendered = render( diff --git a/plugins/techdocs/src/reader/components/useReaderState.test.tsx b/plugins/techdocs/src/reader/components/useReaderState.test.tsx index cce8282b3d..e41a114dbe 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.test.tsx +++ b/plugins/techdocs/src/reader/components/useReaderState.test.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { NotFoundError } from '@backstage/errors'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { techdocsStorageApiRef } from '../../api'; @@ -38,10 +38,10 @@ describe('useReaderState', () => { }; beforeEach(() => { - const apis = ApiRegistry.with(techdocsStorageApiRef, techdocsStorageApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index be1dc658f1..abda1076ae 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -32,7 +32,6 @@ "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.7", "@types/express": "^4.17.6", - "cross-fetch": "^3.0.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "leasot": "^12.0.0", diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index a4221dc710..fe547812ec 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -22,8 +22,8 @@ import OfflineIcon from '@material-ui/icons/Storage'; import React from 'react'; import { EntityTodoContent, todoApiRef, todoPlugin } from '../src'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { Content, Header, HeaderLabel, Page } from '@backstage/core-components'; +import { TestApiProvider } from '@backstage/test-utils'; const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -60,7 +60,7 @@ createDevApp() .registerPlugin(todoPlugin) .addPage({ element: ( - +
@@ -71,7 +71,7 @@ createDevApp() - + ), title: 'Entity Todo Content', icon: OfflineIcon, diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 22a8e6b1e2..9a6098799f 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -28,8 +28,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.4", "@backstage/theme": "^0.2.13", @@ -41,10 +41,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/todo/src/components/TodoList/TodoList.test.tsx b/plugins/todo/src/components/TodoList/TodoList.test.tsx index 0e4d64172d..2759fe40f8 100644 --- a/plugins/todo/src/components/TodoList/TodoList.test.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderWithEffects } from '@backstage/test-utils'; +import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { TodoApi, todoApiRef } from '../../api'; import { TodoList } from './TodoList'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('TodoList', () => { it('should render', async () => { @@ -42,11 +41,11 @@ describe('TodoList', () => { const mockEntity = { metadata: { name: 'mock' } } as Entity; const rendered = await renderWithEffects( - + - , + , ); await expect(rendered.findByText('FIXME')).resolves.toBeInTheDocument(); diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index def12ac1d2..15b1771bde 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx b/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx index 085a884f2e..4e14dec2ef 100644 --- a/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx +++ b/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx @@ -14,22 +14,24 @@ * limitations under the License. */ -import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsAuthProviders } from './UserSettingsAuthProviders'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef, googleAuthApiRef } from '@backstage/core-plugin-api'; const mockSignInHandler = jest.fn().mockReturnValue(''); const mockGoogleAuth = { sessionState$: () => ({ + [Symbol.observable]: jest.fn(), subscribe: () => ({ + closed: false, unsubscribe: () => null, }), }), @@ -47,10 +49,10 @@ const createConfig = () => const config = createConfig(); -const apiRegistry = ApiRegistry.from([ +const apiRegistry = TestApiRegistry.from( [configApiRef, config], [googleAuthApiRef, mockGoogleAuth], -]); +); describe('', () => { it('displays a provider and calls its sign-in handler on click', async () => { diff --git a/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx index caae4d69bc..83d9e23f83 100644 --- a/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx @@ -15,16 +15,16 @@ */ import { AppTheme, appThemeApiRef } from '@backstage/core-plugin-api'; -import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsThemeToggle } from './UserSettingsThemeToggle'; -import { - ApiProvider, - ApiRegistry, - AppThemeSelector, -} from '@backstage/core-app-api'; +import { ApiProvider, AppThemeSelector } from '@backstage/core-app-api'; const mockTheme: AppTheme = { id: 'light-theme', @@ -33,8 +33,9 @@ const mockTheme: AppTheme = { theme: lightTheme, }; -const apiRegistry = ApiRegistry.from([ - [appThemeApiRef, AppThemeSelector.createWithStorage([mockTheme])], +const apiRegistry = TestApiRegistry.from([ + appThemeApiRef, + AppThemeSelector.createWithStorage([mockTheme]), ]); describe('', () => { diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index f29f763666..f14735a448 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.7.4", - "@backstage/core-plugin-api": "^0.2.0", + "@backstage/core-components": "^0.7.5", + "@backstage/core-plugin-api": "^0.2.1", "@backstage/errors": "^0.1.3", "@backstage/theme": "^0.2.13", "@material-ui/core": "^4.12.2", @@ -36,10 +36,10 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.9.0", - "@backstage/core-app-api": "^0.1.22", + "@backstage/cli": "^0.9.1", + "@backstage/core-app-api": "^0.1.23", "@backstage/dev-utils": "^0.2.13", - "@backstage/test-utils": "^0.1.22", + "@backstage/test-utils": "^0.1.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx index 2d949d7abc..7083d890d6 100644 --- a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx +++ b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { BuildDetails, withRequest } from './BuildDetails'; import { xcmetricsApiRef } from '../../api'; @@ -35,11 +34,9 @@ jest.mock('../BuildTimeline', () => ({ describe('BuildDetails', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('accordion-Host')).toBeInTheDocument(); @@ -61,11 +58,9 @@ describe('BuildDetails with request', () => { it('should fetch the build and render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); @@ -78,11 +73,9 @@ describe('BuildDetails with request', () => { .mockRejectedValue({ message: errorMessage }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(errorMessage)).toBeInTheDocument(); @@ -92,11 +85,9 @@ describe('BuildDetails with request', () => { client.XcmetricsClient.getBuild = jest.fn().mockReturnValue(undefined); const rendered = await renderInTestApp( - + - , + , ); expect( diff --git a/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx b/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx index eb141209e7..53471f5e9f 100644 --- a/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx +++ b/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { BuildList } from './BuildList'; import { xcmetricsApiRef } from '../../api'; import userEvent from '@testing-library/user-event'; @@ -35,11 +34,9 @@ jest.mock('../BuildDetails', () => ({ describe('BuildList', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Builds')).toBeInTheDocument(); @@ -50,11 +47,9 @@ describe('BuildList', () => { it('should show build details', async () => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click( @@ -70,11 +65,9 @@ describe('BuildList', () => { .mockRejectedValue({ message }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(message)).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx b/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx index b997de68e0..06f4aed1b7 100644 --- a/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx +++ b/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { BuildListFilter } from './BuildListFilter'; import { BuildFilters, xcmetricsApiRef } from '../../api'; @@ -37,14 +36,12 @@ const renderWithFiltersVisible = async ( callback?: (filters: BuildFilters) => void, ) => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click(rendered.getByLabelText('show filters')); @@ -67,14 +64,12 @@ const setProjectFilter = async (rendered: RenderResult, option: string) => { describe('BuildListFilter', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Filters (0)')).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/Overview/Overview.test.tsx b/plugins/xcmetrics/src/components/Overview/Overview.test.tsx index 3d24d2a422..42e3772d04 100644 --- a/plugins/xcmetrics/src/components/Overview/Overview.test.tsx +++ b/plugins/xcmetrics/src/components/Overview/Overview.test.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { xcmetricsApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { Overview } from './Overview'; jest.mock('../../api/XcmetricsClient'); @@ -33,11 +32,9 @@ jest.mock('../StatusMatrix', () => ({ describe('Overview', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('XCMetrics Dashboard')).toBeInTheDocument(); @@ -49,9 +46,9 @@ describe('Overview', () => { api.getBuilds = jest.fn().mockResolvedValue([]); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('No builds to show')).toBeInTheDocument(); @@ -64,9 +61,9 @@ describe('Overview', () => { api.getBuilds = jest.fn().mockRejectedValue({ message: errorMessage }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(errorMessage)).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx b/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx index 76b47f4943..db7904a8d1 100644 --- a/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx +++ b/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx @@ -15,9 +15,8 @@ */ import React from 'react'; import { OverviewTrends } from './OverviewTrends'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { xcmetricsApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import userEvent from '@testing-library/user-event'; jest.mock('../../api/XcmetricsClient'); @@ -26,11 +25,9 @@ const client = require('../../api/XcmetricsClient'); describe('OverviewTrends', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Trends for')).toBeInTheDocument(); expect(rendered.getAllByText('Build Count').length).toEqual(3); @@ -42,20 +39,18 @@ describe('OverviewTrends', () => { api.getBuildCounts = jest.fn().mockResolvedValue([]); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('--')).toBeInTheDocument(); }); it('should change number of days when select is changed', async () => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click(rendered.getByText('14 days')); @@ -76,9 +71,9 @@ describe('OverviewTrends', () => { .mockRejectedValue({ message: buildTimesError }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(buildCountError)).toBeInTheDocument(); expect(rendered.getByText(buildTimesError)).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx b/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx index cac111d13e..6e5b7f73b9 100644 --- a/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx +++ b/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { StatusCell } from './StatusCell'; import { xcmetricsApiRef } from '../../api'; @@ -27,9 +26,7 @@ const client = require('../../api/XcmetricsClient'); describe('StatusCell', () => { it('should render', async () => { const rendered = await renderInTestApp( - + { size={10} spacing={10} /> - , + , ); userEvent.hover(rendered.getByTestId(client.mockBuild.id)); diff --git a/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx b/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx index 7d3854bbda..f18d322b43 100644 --- a/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx +++ b/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { StatusMatrix } from './StatusMatrix'; import { xcmetricsApiRef } from '../../api'; @@ -25,11 +24,9 @@ const client = require('../../api/XcmetricsClient'); describe('StatusMatrix', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); const cell = rendered.getByTestId(client.mockBuild.id); diff --git a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx index 84bd25624c..29d6407b03 100644 --- a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx +++ b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { XcmetricsLayout } from './XcmetricsLayout'; import { xcmetricsApiRef } from '../../api'; import userEvent from '@testing-library/user-event'; @@ -34,11 +33,9 @@ jest.mock('../BuildList', () => ({ describe('XcmetricsLayout', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Overview')).toBeInTheDocument(); @@ -49,11 +46,9 @@ describe('XcmetricsLayout', () => { it('should show a list of builds when the Builds tab is selected', async () => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click(rendered.getByText('Builds')); diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 22eae267ee..8ca7743cf2 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -35,6 +35,7 @@ import { import { Program } from 'typescript'; import { DocNode, + DocSection, IDocNodeContainerParameters, TSDocTagSyntaxKind, } from '@microsoft/tsdoc'; @@ -50,6 +51,7 @@ import { DocHeading } from '@microsoft/api-documenter/lib/nodes/DocHeading'; import { CustomMarkdownEmitter } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; +import { DocTableCell } from '@microsoft/api-documenter/lib/nodes/DocTableCell'; const tmpDir = resolvePath(__dirname, '../node_modules/.cache/api-extractor'); @@ -603,9 +605,18 @@ async function buildDocs({ }); for (const apiMember of apiModel.members) { + // This is a workaround for this check failing: https://github.com/microsoft/rushstack/blob/915aca8d8847b65981892f44f0544ccb00752792/apps/api-documenter/src/documenters/MarkdownDocumenter.ts#L991 + const description = new DocSection({ configuration }); + if (apiMember.tsdocComment !== undefined) { + this._appendAndMergeSection( + description, + apiMember.tsdocComment.summarySection, + ); + } + const row = new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), - this._createDescriptionCell(apiMember), + new DocTableCell({ configuration }, description.nodes), ]); if (apiMember.kind === 'Package') { diff --git a/yarn.lock b/yarn.lock index 07a5febcb8..1e5d199349 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2078,10 +2078,10 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" - integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.6", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.16.3" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" + integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== dependencies: regenerator-runtime "^0.13.4" @@ -2879,35 +2879,36 @@ resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@gitbeaker/core@^30.2.0", "@gitbeaker/core@^30.3.0": - version "30.3.0" - resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-30.3.0.tgz#d005891d47cfacb41d4a3cc8bf3ee8c68c53d378" - integrity sha512-j7GHsFo6AOuRmLaK4F4Kx967jTy6LFZh5vjmRxjlRvX0qlfl3NJKiQIl30fZz1rPXWdQLAq+kwl1i0BKrWhOpA== +"@gitbeaker/core@^34.6.0": + version "34.6.0" + resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-34.6.0.tgz#f774ea98ac079ba2edf495fdef738ac3f741178b" + integrity sha512-yKF+oxffPyzOnyuHCqLGJrBHhcFHuGHtcmqKhGKtnYPfqcNYA8rt4INAHaE5wMz4ILua9b4sB8p42fki+xn6WA== dependencies: - "@gitbeaker/requester-utils" "^30.3.0" + "@gitbeaker/requester-utils" "^34.6.0" form-data "^4.0.0" li "^1.3.0" + mime "^3.0.0" query-string "^7.0.0" xcase "^2.0.1" -"@gitbeaker/node@^30.2.0": - version "30.3.0" - resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-30.3.0.tgz#ceebde08a13d3f655fa614a4ced56eb1f0a71ec1" - integrity sha512-Ythbadb1+yMO2hSgvp3nvaroHTkI4+mdLg1rdV3YNIF1n+kUYXQD2GY7wpAjH0KjAnBAmgxw/JsFmlfnRu3KAg== +"@gitbeaker/node@^34.6.0": + version "34.6.0" + resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-34.6.0.tgz#104f122433b65ceb45b0e645001d15cbcc9b1280" + integrity sha512-gVV4Wuev43Jbyoy1fszC885+bkvWH4zWiUhtIu0PSAm628j/OxO7idLIqUEMV0hDf6wm/PE/vOSP6PhjE0N+fA== dependencies: - "@gitbeaker/core" "^30.3.0" - "@gitbeaker/requester-utils" "^30.3.0" + "@gitbeaker/core" "^34.6.0" + "@gitbeaker/requester-utils" "^34.6.0" delay "^5.0.0" got "^11.8.2" xcase "^2.0.1" -"@gitbeaker/requester-utils@^30.3.0": - version "30.3.0" - resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-30.3.0.tgz#fbbaae20263ff90952da116d8782f74f47b29c81" - integrity sha512-b5vCaUqP2Jrqdpt5CkmFET4VFHwJYjnIovwZGYd5H0aKmiPw4NmeW1JtP+65J4xf0c4AOQQj6XSf7TZLuPNAqw== +"@gitbeaker/requester-utils@^34.6.0": + version "34.6.0" + resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-34.6.0.tgz#4489009b759ca6f9a83f244453f4f610f1ac7349" + integrity sha512-H8utxbSP1kEdX0KcyVYrTDTT0A3UcPwrIV1ahyufX9ZLybYSUsA56B8Wx5kJSbWGFT1ffu2f8H2YDMwNCKKsBg== dependencies: form-data "^4.0.0" - query-string "^7.0.0" + qs "^6.10.1" xcase "^2.0.1" "@google-cloud/common@^3.7.0": @@ -3654,6 +3655,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^27.2.5": + version "27.2.5" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132" + integrity sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@josephg/resolvable@^1.0.0": version "1.0.1" resolved "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz#69bc4db754d79e1a2f17a650d3466e038d94a5eb" @@ -5556,16 +5568,16 @@ util-deprecate "^1.0.2" "@storybook/addon-actions@^6.1.11": - version "6.3.7" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.7.tgz#b25434972bef351aceb3f7ec6fd66e210f256aac" - integrity sha512-CEAmztbVt47Gw1o6Iw0VP20tuvISCEKk9CS/rCjHtb4ubby6+j/bkp3pkEUQIbyLdHiLWFMz0ZJdyA/U6T6jCw== + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.12.tgz#69eb5f8f780f1b00456051da6290d4b959ba24a0" + integrity sha512-mzuN4Ano4eyicwycM2PueGzzUCAEzt9/6vyptWEIVJu0sjK0J9KtBRlqFi1xGQxmCfimDR/n/vWBBkc7fp2uJA== dependencies: - "@storybook/addons" "6.3.7" - "@storybook/api" "6.3.7" - "@storybook/client-api" "6.3.7" - "@storybook/components" "6.3.7" - "@storybook/core-events" "6.3.7" - "@storybook/theming" "6.3.7" + "@storybook/addons" "6.3.12" + "@storybook/api" "6.3.12" + "@storybook/client-api" "6.3.12" + "@storybook/components" "6.3.12" + "@storybook/core-events" "6.3.12" + "@storybook/theming" "6.3.12" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -5841,19 +5853,6 @@ qs "^6.10.0" telejson "^5.3.2" -"@storybook/channel-postmessage@6.3.7": - version "6.3.7" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.7.tgz#bd4edf84a29aa2cd4a22d26115c60194d289a840" - integrity sha512-Cmw8HRkeSF1yUFLfEIUIkUICyCXX8x5Ol/5QPbiW9HPE2hbZtYROCcg4bmWqdq59N0Tp9FQNSn+9ZygPgqQtNw== - dependencies: - "@storybook/channels" "6.3.7" - "@storybook/client-logger" "6.3.7" - "@storybook/core-events" "6.3.7" - core-js "^3.8.2" - global "^4.4.0" - qs "^6.10.0" - telejson "^5.3.2" - "@storybook/channels@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.11.tgz#a14ce233367a9072bd1cffef3825f125c27bb0ae" @@ -5929,30 +5928,6 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.3.7": - version "6.3.7" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.7.tgz#cb1dca05467d777bd09aadbbdd1dd22ca537ce14" - integrity sha512-8wOH19cMIwIIYhZy5O5Wl8JT1QOL5kNuamp9GPmg5ff4DtnG+/uUslskRvsnKyjPvl+WbIlZtBVWBiawVdd/yQ== - dependencies: - "@storybook/addons" "6.3.7" - "@storybook/channel-postmessage" "6.3.7" - "@storybook/channels" "6.3.7" - "@storybook/client-logger" "6.3.7" - "@storybook/core-events" "6.3.7" - "@storybook/csf" "0.0.1" - "@types/qs" "^6.9.5" - "@types/webpack-env" "^1.16.0" - core-js "^3.8.2" - global "^4.4.0" - lodash "^4.17.20" - memoizerific "^1.11.3" - qs "^6.10.0" - regenerator-runtime "^0.13.7" - stable "^0.1.8" - store2 "^2.12.0" - ts-dedent "^2.0.0" - util-deprecate "^1.0.2" - "@storybook/client-logger@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.11.tgz#d2e0e17f35ed4c5ee282d818b6db3a015ce6b833" @@ -6651,13 +6626,13 @@ dependencies: defer-to-connect "^2.0.0" -"@testing-library/cypress@^7.0.1": - version "7.0.6" - resolved "https://registry.npmjs.org/@testing-library/cypress/-/cypress-7.0.6.tgz#5445dac4f4852c26901c356e9d3a69371bd20ccf" - integrity sha512-atnjqlkEt6spU4Mv7evvpA8fMXeRw7AN2uTKOR1dP6WBvBixVwAYMZY+1fMOaZULWAj9vGLCXXvmw++u3TxuCQ== +"@testing-library/cypress@^8.0.2": + version "8.0.2" + resolved "https://registry.npmjs.org/@testing-library/cypress/-/cypress-8.0.2.tgz#b13f0ff2424dec4368b6670dfbfb7e43af8eefc9" + integrity sha512-KVdm7n37sg/A4e3wKMD4zUl0NpzzVhx06V9Tf0hZHZ7nrZ4yFva6Zwg2EFF1VzHkEfN/ahUzRtT1qiW+vuWnJw== dependencies: - "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^7.28.1" + "@babel/runtime" "^7.14.6" + "@testing-library/dom" "^8.1.0" "@testing-library/dom@^7.28.1": version "7.29.6" @@ -6673,6 +6648,20 @@ lz-string "^1.4.4" pretty-format "^26.6.2" +"@testing-library/dom@^8.1.0": + version "8.11.1" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.1.tgz#03fa2684aa09ade589b460db46b4c7be9fc69753" + integrity sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + "@testing-library/jest-dom@^5.10.1": version "5.14.1" resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz#8501e16f1e55a55d675fe73eecee32cdaddb9766" @@ -6742,6 +6731,11 @@ axios-cached-dns-resolve "0.5.2" file-type "16.5.3" +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -7623,6 +7617,14 @@ "@types/node" "*" form-data "^3.0.0" +"@types/node-fetch@^2.5.12": + version "2.5.12" + resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" + integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32": version "14.17.8" resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" @@ -8317,6 +8319,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@types/yarnpkg__lockfile@^1.1.4": version "1.1.4" resolved "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" @@ -8831,7 +8840,7 @@ add-stream@^1.0.0: resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= -address@1.1.2, address@^1.0.1: +address@1.1.2, address@^1.0.1, address@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== @@ -8938,7 +8947,7 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" -alphanum-sort@^1.0.0: +alphanum-sort@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= @@ -9011,7 +9020,7 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-regex@^5.0.0: +ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -9041,6 +9050,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-to-html@^0.6.11: version "0.6.14" resolved "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8" @@ -9362,6 +9376,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -10388,7 +10407,7 @@ browserslist@4.14.2: escalade "^3.0.2" node-releases "^1.1.61" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.6: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.5, browserslist@^4.16.6: version "4.18.1" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== @@ -10670,25 +10689,6 @@ call-me-maybe@^1.0.1: resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -11332,7 +11332,7 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2, color-string@^1.5.4, color-string@^1.6.0: +color-string@^1.5.2, color-string@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312" integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA== @@ -11348,14 +11348,6 @@ color@3.0.x: color-convert "^1.9.1" color-string "^1.5.2" -color@^3.0.0: - version "3.1.3" - resolved "https://registry.npmjs.org/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" - integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== - dependencies: - color-convert "^1.9.1" - color-string "^1.5.4" - color@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67" @@ -11364,6 +11356,11 @@ color@^4.0.1: color-convert "^2.0.1" color-string "^1.6.0" +colord@^2.9.1: + version "2.9.1" + resolved "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e" + integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw== + colorette@1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" @@ -11878,16 +11875,6 @@ cosmiconfig@7.0.0, cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -11991,6 +11978,11 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cronstrue@^1.122.0: + version "1.122.0" + resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-1.122.0.tgz#bd6838077b476d28f61d381398b47b8c3912a126" + integrity sha512-PFuhZd+iPQQ0AWTXIEYX+t3nFGzBrWxmTWUKJOrsGRewaBSLKZ4I1f8s2kryU75nNxgyugZgiGh2OJsCTA/XlA== + cross-env@^7.0.0: version "7.0.3" resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -12084,17 +12076,11 @@ css-color-converter@^2.0.0: color-name "^1.1.4" css-unit-converter "^1.1.2" -css-color-names@0.0.4, css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-declaration-sorter@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" - integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== +css-declaration-sorter@^6.0.3: + version "6.1.3" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz#e9852e4cf940ba79f509d9425b137d1f94438dc2" + integrity sha512-SvjQjNRZgh4ULK1LDJ2AduPKUKxIqmtU7ZAyi47BTV+M90Qvxr9AB6lKlLbDUfXqI9IQeYA8LbAsCZPpJEV3aA== dependencies: - postcss "^7.0.1" timsort "^0.3.0" css-in-js-utils@^2.0.0: @@ -12182,6 +12168,14 @@ css-tree@^1.1.2: mdn-data "2.0.14" source-map "^0.6.1" +css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + css-unit-converter@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" @@ -12229,73 +12223,55 @@ cssfilter@0.0.10: resolved "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4= -cssnano-preset-default@^4.0.7: - version "4.0.7" - resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" - integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== +cssnano-preset-default@^5.1.7: + version "5.1.7" + resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.7.tgz#68c3ad1ec6a810482ec7d06b2d70fc34b6b0d70c" + integrity sha512-bWDjtTY+BOqrqBtsSQIbN0RLGD2Yr2CnecpP0ydHNafh9ZUEre8c8VYTaH9FEbyOt0eIfEUAYYk5zj92ioO8LA== dependencies: - css-declaration-sorter "^4.0.1" - cssnano-util-raw-cache "^4.0.1" - postcss "^7.0.0" - postcss-calc "^7.0.1" - postcss-colormin "^4.0.3" - postcss-convert-values "^4.0.1" - postcss-discard-comments "^4.0.2" - postcss-discard-duplicates "^4.0.2" - postcss-discard-empty "^4.0.1" - postcss-discard-overridden "^4.0.1" - postcss-merge-longhand "^4.0.11" - postcss-merge-rules "^4.0.3" - postcss-minify-font-values "^4.0.2" - postcss-minify-gradients "^4.0.2" - postcss-minify-params "^4.0.2" - postcss-minify-selectors "^4.0.2" - postcss-normalize-charset "^4.0.1" - postcss-normalize-display-values "^4.0.2" - postcss-normalize-positions "^4.0.2" - postcss-normalize-repeat-style "^4.0.2" - postcss-normalize-string "^4.0.2" - postcss-normalize-timing-functions "^4.0.2" - postcss-normalize-unicode "^4.0.1" - postcss-normalize-url "^4.0.1" - postcss-normalize-whitespace "^4.0.2" - postcss-ordered-values "^4.1.2" - postcss-reduce-initial "^4.0.3" - postcss-reduce-transforms "^4.0.2" - postcss-svgo "^4.0.2" - postcss-unique-selectors "^4.0.1" + css-declaration-sorter "^6.0.3" + cssnano-utils "^2.0.1" + postcss-calc "^8.0.0" + postcss-colormin "^5.2.1" + postcss-convert-values "^5.0.2" + postcss-discard-comments "^5.0.1" + postcss-discard-duplicates "^5.0.1" + postcss-discard-empty "^5.0.1" + postcss-discard-overridden "^5.0.1" + postcss-merge-longhand "^5.0.4" + postcss-merge-rules "^5.0.3" + postcss-minify-font-values "^5.0.1" + postcss-minify-gradients "^5.0.3" + postcss-minify-params "^5.0.2" + postcss-minify-selectors "^5.1.0" + postcss-normalize-charset "^5.0.1" + postcss-normalize-display-values "^5.0.1" + postcss-normalize-positions "^5.0.1" + postcss-normalize-repeat-style "^5.0.1" + postcss-normalize-string "^5.0.1" + postcss-normalize-timing-functions "^5.0.1" + postcss-normalize-unicode "^5.0.1" + postcss-normalize-url "^5.0.3" + postcss-normalize-whitespace "^5.0.1" + postcss-ordered-values "^5.0.2" + postcss-reduce-initial "^5.0.1" + postcss-reduce-transforms "^5.0.1" + postcss-svgo "^5.0.3" + postcss-unique-selectors "^5.0.2" -cssnano-util-get-arguments@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" - integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= +cssnano-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2" + integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ== -cssnano-util-get-match@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" - integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= - -cssnano-util-raw-cache@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" - integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== +cssnano@^5.0.1: + version "5.0.11" + resolved "https://registry.npmjs.org/cssnano/-/cssnano-5.0.11.tgz#743397a05e04cb87e9df44b7659850adfafc3646" + integrity sha512-5SHM31NAAe29jvy0MJqK40zZ/8dGlnlzcfHKw00bWMVFp8LWqtuyPSFwbaoIoxvt71KWJOfg8HMRGrBR3PExCg== dependencies: - postcss "^7.0.0" - -cssnano-util-same-parent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" - integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== - -cssnano@^4.1.10: - version "4.1.10" - resolved "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" - integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== - dependencies: - cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.7" - is-resolvable "^1.0.0" - postcss "^7.0.0" + cssnano-preset-default "^5.1.7" + is-resolvable "^1.1.0" + lilconfig "^2.0.3" + yaml "^1.10.2" csso@^4.0.2: version "4.0.2" @@ -12304,6 +12280,13 @@ csso@^4.0.2: dependencies: css-tree "1.0.0-alpha.37" +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + cssom@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -13006,7 +12989,7 @@ detect-node@^2.0.4: resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== -detect-port-alt@1.1.6: +detect-port-alt@1.1.6, detect-port-alt@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== @@ -13162,6 +13145,11 @@ dom-accessibility-api@^0.5.4, dom-accessibility-api@^0.5.6: resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz#3f5d43b52c7a3bd68b5fb63fa47b4e4c1fdf65a9" integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== +dom-accessibility-api@^0.5.9: + version "0.5.10" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz#caa6d08f60388d0bb4539dd75fe458a9a1d0014c" + integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -13724,6 +13712,114 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" +esbuild-android-arm64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.14.tgz#c85083ece26be3d67e6c720e088968a98409e023" + integrity sha512-Q+Xhfp827r+ma8/DJgpMRUbDZfefsk13oePFEXEIJ4gxFbNv5+vyiYXYuKm43/+++EJXpnaYmEnu4hAKbAWYbA== + +esbuild-darwin-64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.14.tgz#8e4e237ad847cc54a1d3a5caee26a746b9f0b81f" + integrity sha512-YmOhRns6QBNSjpVdTahi/yZ8dscx9ai7a6OY6z5ACgOuQuaQ2Qk2qgJ0/siZ6LgD0gJFMV8UINFV5oky5TFNQQ== + +esbuild-darwin-arm64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.14.tgz#b3b5ebd40b2cb06ee0f6fb342dd4bdcca54ad273" + integrity sha512-Lp00VTli2jqZghSa68fx3fEFCPsO1hK59RMo1PRap5RUjhf55OmaZTZYnCDI0FVlCtt+gBwX5qwFt4lc6tI1xg== + +esbuild-freebsd-64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.14.tgz#175ecb2fa8141428cf70ea2d5f4c27534bad53e0" + integrity sha512-BKosI3jtvTfnmsCW37B1TyxMUjkRWKqopR0CE9AF2ratdpkxdR24Vpe3gLKNyWiZ7BE96/SO5/YfhbPUzY8wKw== + +esbuild-freebsd-arm64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.14.tgz#a7d64e41d1fa581f8db7775e5200f18e67d70c4d" + integrity sha512-yd2uh0yf+fWv5114+SYTl4/1oDWtr4nN5Op+PGxAkMqHfYfLjFKpcxwCo/QOS/0NWqPVE8O41IYZlFhbEN2B8Q== + +esbuild-linux-32@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.14.tgz#14bdd4f6b6cfd35c65c835894651ba335c2117da" + integrity sha512-a8rOnS1oWSfkkYWXoD2yXNV4BdbDKA7PNVQ1klqkY9SoSApL7io66w5H44mTLsfyw7G6Z2vLlaLI2nz9MMAowA== + +esbuild-linux-64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.14.tgz#7fd56851b2982fdd0cd8447ee9858c2c5711708a" + integrity sha512-yPZSoMs9W2MC3Dw+6kflKt5FfQm6Dicex9dGIr1OlHRsn3Hm7yGMUTctlkW53KknnZdOdcdd5upxvbxqymczVQ== + +esbuild-linux-arm64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.14.tgz#a55634d70679ba509adeafd68eebb9fd1ec5af6c" + integrity sha512-Lvo391ln9PzC334e+jJ2S0Rt0cxP47eoH5gFyv/E8HhOnEJTvm7A+RRnMjjHnejELacTTfYgFGQYPjLsi/jObQ== + +esbuild-linux-arm@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.14.tgz#bb96a99677e608b31ff61f37564326d38e846ca2" + integrity sha512-8chZE4pkKRvJ/M/iwsNQ1KqsRg2RyU5eT/x2flNt/f8F2TVrDreR7I0HEeCR50wLla3B1C3wTIOzQBmjuc6uWg== + +esbuild-linux-mips64le@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.14.tgz#6a55362a8fd1e593dea2ecc41877beed8b8184b9" + integrity sha512-MZhgxbmrWbpY3TOE029O6l5tokG9+Yoj2hW7vdit/d/VnmneqeGrSHADuDL6qXM8L5jaCiaivb4VhsyVCpdAbQ== + +esbuild-linux-ppc64le@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.14.tgz#9e0048587ece0a7f184ab147f20d077098045e7f" + integrity sha512-un7KMwS7fX1Un6BjfSZxTT8L5cV/8Uf4SAhM7WYy2XF8o8TI+uRxxD03svZnRNIPsN2J5cl6qV4n7Iwz+yhhVw== + +esbuild-netbsd-64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.14.tgz#dcab16a4bbcfa16e2e8535dadc5f64fdc891c63b" + integrity sha512-5ekKx/YbOmmlTeNxBjh38Uh5TGn5C4uyqN17i67k18pS3J+U2hTVD7rCxcFcRS1AjNWumkVL3jWqYXadFwMS0Q== + +esbuild-openbsd-64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.14.tgz#3c7453b155ebb68dc34d5aec3bd6505337bdda08" + integrity sha512-9bzvwewHjct2Cv5XcVoE1yW5YTW12Sk838EYfA46abgnhxGoFSD1mFcaztp5HHC43AsF+hQxbSFG/RilONARUA== + +esbuild-sunos-64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.14.tgz#85addf5fef6b5db154a955d4f2e88953359d75ce" + integrity sha512-mjMrZB76M6FmoiTvj/RGWilrioR7gVwtFBRVugr9qLarXMIU1W/pQx+ieEOtflrW61xo8w1fcxyHsVVGRvoQ0w== + +esbuild-windows-32@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.14.tgz#f77f98f30a5c636c44db2428ecdf9bcbbaedb1a7" + integrity sha512-GZa6mrx2rgfbH/5uHg0Rdw50TuOKbdoKCpEBitzmG5tsXBdce+cOL+iFO5joZc6fDVCLW3Y6tjxmSXRk/v20Hg== + +esbuild-windows-64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.14.tgz#bc778674c40d65150d12385e0f23eb3a0badbd0d" + integrity sha512-Lsgqah24bT7ClHjLp/Pj3A9wxjhIAJyWQcrOV4jqXAFikmrp2CspA8IkJgw7HFjx6QrJuhpcKVbCAe/xw0i2yw== + +esbuild-windows-arm64@0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.14.tgz#91a8dad35ab2c4dd27cd83860742955b25a354d7" + integrity sha512-KP8FHVlWGhM7nzYtURsGnskXb/cBCPTfj0gOKfjKq2tHtYnhDZywsUG57nk7TKhhK0fL11LcejHG3LRW9RF/9A== + +esbuild@^0.13.14: + version "0.13.14" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.13.14.tgz#98a3f7f42809abdc2b57c84565d0f713382dc1a5" + integrity sha512-xu4D+1ji9x53ocuomcY+KOrwAnWzhBu/wTEjpdgZ8I1c8i5vboYIeigMdzgY1UowYBKa2vZgVgUB32bu7gkxeg== + optionalDependencies: + esbuild-android-arm64 "0.13.14" + esbuild-darwin-64 "0.13.14" + esbuild-darwin-arm64 "0.13.14" + esbuild-freebsd-64 "0.13.14" + esbuild-freebsd-arm64 "0.13.14" + esbuild-linux-32 "0.13.14" + esbuild-linux-64 "0.13.14" + esbuild-linux-arm "0.13.14" + esbuild-linux-arm64 "0.13.14" + esbuild-linux-mips64le "0.13.14" + esbuild-linux-ppc64le "0.13.14" + esbuild-netbsd-64 "0.13.14" + esbuild-openbsd-64 "0.13.14" + esbuild-sunos-64 "0.13.14" + esbuild-windows-32 "0.13.14" + esbuild-windows-64 "0.13.14" + esbuild-windows-arm64 "0.13.14" + esbuild@^0.8.56: version "0.8.57" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.57.tgz#a42d02bc2b57c70bcd0ef897fe244766bb6dd926" @@ -14676,6 +14772,11 @@ filesize@6.1.0: resolved "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00" integrity sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg== +filesize@^6.1.0: + version "6.4.0" + resolved "https://registry.npmjs.org/filesize/-/filesize-6.4.0.tgz#914f50471dd66fdca3cefe628bd0cde4ef769bcd" + integrity sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -14910,6 +15011,25 @@ fork-ts-checker-webpack-plugin@^6.0.4: semver "^7.3.2" tapable "^1.0.0" +fork-ts-checker-webpack-plugin@^6.0.5: + version "6.4.2" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.4.2.tgz#6d307fb4072ce4abe4d56a89c8ef060066f33d81" + integrity sha512-EqtzzRdx2mldr0KEydSN9jaNrf419gMpwkloumG6K/S7jtJc9Fl7wMJ+y+o7DLLGMMU/kouYr06agTD/YkxzIQ== + dependencies: + "@babel/code-frame" "^7.8.3" + "@types/json-schema" "^7.0.5" + chalk "^4.1.0" + chokidar "^3.4.2" + cosmiconfig "^6.0.0" + deepmerge "^4.2.2" + fs-extra "^9.0.0" + glob "^7.1.6" + memfs "^3.1.2" + minimatch "^3.0.4" + schema-utils "2.7.0" + semver "^7.3.2" + tapable "^1.0.0" + form-data-encoder@^1.4.3: version "1.6.0" resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.6.0.tgz#9dd1f479836c1b1b47201667c68f8daafa800943" @@ -15488,7 +15608,7 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" -global-modules@2.0.0: +global-modules@2.0.0, global-modules@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== @@ -15932,7 +16052,7 @@ gud@^1.0.0: resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== -gzip-size@5.1.1: +gzip-size@5.1.1, gzip-size@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== @@ -16055,7 +16175,7 @@ has-yarn@^2.1.0: resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== -has@^1.0.0, has@^1.0.3: +has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -16179,11 +16299,6 @@ helmet@^4.0.0: resolved "https://registry.npmjs.org/helmet/-/helmet-4.4.1.tgz#a17e1444d81d7a83ddc6e6f9bc6e2055b994efe7" integrity sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw== -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - highlight.js@^10.1.0, highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@^10.6.0: version "10.7.2" resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360" @@ -16256,16 +16371,6 @@ hpagent@^0.1.1: resolved "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ== -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -16617,10 +16722,10 @@ immer@8.0.1: resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== -immer@^9.0.1: - version "9.0.6" - resolved "https://registry.npmjs.org/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" - integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ== +immer@^9.0.1, immer@^9.0.6: + version "9.0.7" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz#b6156bd7db55db7abc73fd2fdadf4e579a701075" + integrity sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA== immutable@>=3.8.2, immutable@^3.8.2, immutable@^3.x.x: version "3.8.2" @@ -16639,14 +16744,6 @@ import-cwd@^3.0.0: dependencies: import-from "^3.0.0" -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" @@ -16916,10 +17013,10 @@ ipaddr.js@^2.0.1: resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== -is-absolute-url@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" - integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== is-absolute@^1.0.0: version "1.0.0" @@ -17039,18 +17136,6 @@ is-ci@^3.0.0: dependencies: ci-info "^3.1.1" -is-color-stop@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - is-core-module@^2.1.0, is-core-module@^2.2.0: version "2.4.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" @@ -17105,11 +17190,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -17423,12 +17503,12 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" -is-resolvable@^1.0.0: +is-resolvable@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== -is-root@2.1.0: +is-root@2.1.0, is-root@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== @@ -19001,6 +19081,11 @@ libnpmpublish@^4.0.0: semver "^7.1.3" ssri "^8.0.0" +lilconfig@^2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" + integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -20547,6 +20632,11 @@ mime@^2.2.0, mime@^2.4.4, mime@^2.4.6: resolved "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -21134,13 +21224,6 @@ node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-fetch@2.6.5: - version "2.6.5" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" - integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== - dependencies: - whatwg-url "^5.0.0" - node-forge@^0.10.0: version "0.10.0" resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -21361,7 +21444,7 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= -normalize-url@^3.0.0, normalize-url@^3.3.0: +normalize-url@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== @@ -21371,6 +21454,11 @@ normalize-url@^4.1.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + npm-bundled@^1.0.1, npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" @@ -22763,7 +22851,7 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -pkg-up@3.1.0: +pkg-up@3.1.0, pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== @@ -22820,61 +22908,50 @@ posix-character-classes@^0.1.0: resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-calc@^7.0.1: - version "7.0.2" - resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz#504efcd008ca0273120568b0792b16cdcde8aac1" - integrity sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ== +postcss-calc@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a" + integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g== dependencies: - postcss "^7.0.27" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.2" -postcss-colormin@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" - integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== +postcss-colormin@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.1.tgz#6e444a806fd3c578827dbad022762df19334414d" + integrity sha512-VVwMrEYLcHYePUYV99Ymuoi7WhKrMGy/V9/kTS0DkCoJYmmjdOMneyhzYUxcNgteKDVbrewOkSM7Wje/MFwxzA== dependencies: - browserslist "^4.0.0" - color "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + browserslist "^4.16.6" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.1.0" -postcss-convert-values@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" - integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== +postcss-convert-values@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059" + integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-discard-comments@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" - integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== - dependencies: - postcss "^7.0.0" +postcss-discard-comments@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe" + integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg== -postcss-discard-duplicates@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" - integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== - dependencies: - postcss "^7.0.0" +postcss-discard-duplicates@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d" + integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA== -postcss-discard-empty@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" - integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== - dependencies: - postcss "^7.0.0" +postcss-discard-empty@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8" + integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw== -postcss-discard-overridden@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" - integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== - dependencies: - postcss "^7.0.0" +postcss-discard-overridden@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" + integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== postcss-flexbugs-fixes@^4.2.1: version "4.2.1" @@ -22902,67 +22979,57 @@ postcss-loader@^4.2.0: schema-utils "^3.0.0" semver "^7.3.4" -postcss-merge-longhand@^4.0.11: - version "4.0.11" - resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" - integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== +postcss-merge-longhand@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.4.tgz#41f4f3270282ea1a145ece078b7679f0cef21c32" + integrity sha512-2lZrOVD+d81aoYkZDpWu6+3dTAAGkCKbV5DoRhnIR7KOULVrI/R7bcMjhrH9KTRy6iiHKqmtG+n/MMj1WmqHFw== dependencies: - css-color-names "0.0.4" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - stylehacks "^4.0.0" + postcss-value-parser "^4.1.0" + stylehacks "^5.0.1" -postcss-merge-rules@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" - integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== +postcss-merge-rules@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.3.tgz#b5cae31f53129812a77e3eb1eeee448f8cf1a1db" + integrity sha512-cEKTMEbWazVa5NXd8deLdCnXl+6cYG7m2am+1HzqH0EnTdy8fRysatkaXb2dEnR+fdaDxTvuZ5zoBdv6efF6hg== dependencies: - browserslist "^4.0.0" + browserslist "^4.16.6" caniuse-api "^3.0.0" - cssnano-util-same-parent "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - vendors "^1.0.0" + cssnano-utils "^2.0.1" + postcss-selector-parser "^6.0.5" -postcss-minify-font-values@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" - integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== +postcss-minify-font-values@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf" + integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-minify-gradients@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" - integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== +postcss-minify-gradients@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e" + integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q== dependencies: - cssnano-util-get-arguments "^4.0.0" - is-color-stop "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + colord "^2.9.1" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-minify-params@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" - integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== +postcss-minify-params@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.2.tgz#1b644da903473fbbb18fbe07b8e239883684b85c" + integrity sha512-qJAPuBzxO1yhLad7h2Dzk/F7n1vPyfHfCCh5grjGfjhi1ttCnq4ZXGIW77GSrEbh9Hus9Lc/e/+tB4vh3/GpDg== dependencies: - alphanum-sort "^1.0.0" - browserslist "^4.0.0" - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - uniqs "^2.0.0" + alphanum-sort "^1.0.2" + browserslist "^4.16.6" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-minify-selectors@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" - integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== +postcss-minify-selectors@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54" + integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og== dependencies: - alphanum-sort "^1.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" postcss-modules-extract-imports@^2.0.0: version "2.0.0" @@ -23039,124 +23106,96 @@ postcss-modules@^4.0.0: postcss-modules-values "^4.0.0" string-hash "^1.1.1" -postcss-normalize-charset@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" - integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== - dependencies: - postcss "^7.0.0" +postcss-normalize-charset@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" + integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg== -postcss-normalize-display-values@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" - integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== +postcss-normalize-display-values@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd" + integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ== dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-positions@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" - integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== +postcss-normalize-positions@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5" + integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg== dependencies: - cssnano-util-get-arguments "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-normalize-repeat-style@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" - integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== +postcss-normalize-repeat-style@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5" + integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w== dependencies: - cssnano-util-get-arguments "^4.0.0" - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-string@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" - integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== +postcss-normalize-string@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0" + integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA== dependencies: - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-normalize-timing-functions@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" - integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== +postcss-normalize-timing-functions@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c" + integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q== dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-unicode@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" - integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== +postcss-normalize-unicode@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37" + integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA== dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + browserslist "^4.16.0" + postcss-value-parser "^4.1.0" -postcss-normalize-url@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" - integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== +postcss-normalize-url@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.3.tgz#42eca6ede57fe69075fab0f88ac8e48916ef931c" + integrity sha512-qWiUMbvkRx3kc1Dp5opzUwc7MBWZcSDK2yofCmdvFBCpx+zFPkxBC1FASQ59Pt+flYfj/nTZSkmF56+XG5elSg== dependencies: - is-absolute-url "^2.0.0" - normalize-url "^3.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + is-absolute-url "^3.0.3" + normalize-url "^6.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-whitespace@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" - integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== +postcss-normalize-whitespace@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a" + integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-ordered-values@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" - integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== +postcss-ordered-values@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044" + integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ== dependencies: - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-reduce-initial@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" - integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== +postcss-reduce-initial@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946" + integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw== dependencies: - browserslist "^4.0.0" + browserslist "^4.16.0" caniuse-api "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" -postcss-reduce-transforms@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" - integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== +postcss-reduce-transforms@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640" + integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA== dependencies: - cssnano-util-get-match "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-selector-parser@^3.0.0: - version "3.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" - integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== - dependencies: - dot-prop "^5.2.0" - indexes-of "^1.0.1" - uniq "^1.0.1" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.4" @@ -23168,35 +23207,36 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector uniq "^1.0.1" util-deprecate "^1.0.2" -postcss-svgo@^4.0.2: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" - integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== +postcss-selector-parser@^6.0.5: + version "6.0.6" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" + integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - svgo "^1.0.0" + cssesc "^3.0.0" + util-deprecate "^1.0.2" -postcss-unique-selectors@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" - integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== +postcss-svgo@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30" + integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA== dependencies: - alphanum-sort "^1.0.0" - postcss "^7.0.0" - uniqs "^2.0.0" + postcss-value-parser "^4.1.0" + svgo "^2.7.0" -postcss-value-parser@^3.0.0: - version "3.3.1" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== +postcss-unique-selectors@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.2.tgz#5d6893daf534ae52626708e0d62250890108c0c1" + integrity sha512-w3zBVlrtZm7loQWRPVC0yjUwwpty7OM6DnEHkxcSQXO1bMS3RJ+JUS5LFMSDZHJcvGsRwhZinCWVqn8Kej4EDA== + dependencies: + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: +"postcss@5 - 7", postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.32" resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -23347,6 +23387,16 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^27.0.2: + version "27.3.1" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz#7e9486365ccdd4a502061fa761d3ab9ca1b78df5" + integrity sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA== + dependencies: + "@jest/types" "^27.2.5" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -23879,7 +23929,7 @@ react-debounce-input@=3.2.4: lodash.debounce "^4" prop-types "^15.7.2" -react-dev-utils@^11.0.3, react-dev-utils@^11.0.4: +react-dev-utils@^11.0.3: version "11.0.4" resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a" integrity sha512-dx0LvIGHcOPtKbeiSUM4jqpBl3TcY7CDjZdfOIcKeznE7BWr9dg0iPG90G5yfVQ+p/rGNMXdbfStvzQZEVEi4A== @@ -23909,6 +23959,36 @@ react-dev-utils@^11.0.3, react-dev-utils@^11.0.4: strip-ansi "6.0.0" text-table "0.2.0" +react-dev-utils@^12.0.0-next.47: + version "12.0.0-next.47" + resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0-next.47.tgz#e55c31a05eb30cfd69ca516e8b87d61724e880fb" + integrity sha512-PsE71vP15TZMmp/RZKOJC4fYD5Pvt0+wCoyG3QHclto0d4FyIJI78xGRICOOThZFROqgXYlZP6ddmeybm+jO4w== + dependencies: + "@babel/code-frame" "^7.10.4" + address "^1.1.2" + browserslist "^4.16.5" + chalk "^2.4.2" + cross-spawn "^7.0.3" + detect-port-alt "^1.1.6" + escape-string-regexp "^2.0.0" + filesize "^6.1.0" + find-up "^4.1.0" + fork-ts-checker-webpack-plugin "^6.0.5" + global-modules "^2.0.0" + globby "^11.0.1" + gzip-size "^5.1.1" + immer "^9.0.6" + is-root "^2.1.0" + loader-utils "^2.0.0" + open "^7.0.2" + pkg-up "^3.1.0" + prompts "^2.4.0" + react-error-overlay "7.0.0-next.54+1465357b" + recursive-readdir "^2.2.2" + shell-quote "^1.7.2" + strip-ansi "^6.0.0" + text-table "^0.2.0" + react-docgen-typescript@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.1.0.tgz#20db64a7fd62e63a8a9469fb4abd90600878cbb2" @@ -23958,6 +24038,11 @@ react-error-boundary@^3.1.0: dependencies: "@babel/runtime" "^7.12.5" +react-error-overlay@7.0.0-next.54+1465357b: + version "7.0.0-next.54" + resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-7.0.0-next.54.tgz#c1eb5ab86aee15e9552e6d97897b08f2bd06d140" + integrity sha512-b96CiTnZahXPDNH9MKplvt5+jD+BkxDw7q5R3jnkUXze/ux1pLv32BBZmlj0OfCUeMqyz4sAmF+0ccJGVMlpXw== + react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" @@ -25072,11 +25157,6 @@ resolve-from@5.0.0, resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -25188,16 +25268,6 @@ rfc4648@^1.3.0: resolved "https://registry.npmjs.org/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - rifm@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" @@ -25265,13 +25335,13 @@ rollup-plugin-peer-deps-external@^2.2.2: integrity sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g== rollup-plugin-postcss@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.0.tgz#2131fb6db0d5dce01a37235e4f6ad4523c681cea" - integrity sha512-OQzT+YspV01/6dxfyEw8lBO2px3hyL8Xn+k2QGctL7V/Yx2Z1QaMKdYVslP1mqv7RsKt6DROIlnbpmgJ3yxf6g== + version "4.0.2" + resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz#15e9462f39475059b368ce0e49c800fa4b1f7050" + integrity sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w== dependencies: chalk "^4.1.0" concat-with-sourcemaps "^1.1.0" - cssnano "^4.1.10" + cssnano "^5.0.1" import-cwd "^3.0.0" p-queue "^6.6.2" pify "^5.0.0" @@ -25476,7 +25546,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: +schema-utils@^2.6.5, schema-utils@^2.7.0: version "2.7.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -25788,6 +25858,11 @@ shell-quote@1.7.2: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +shell-quote@^1.7.2: + version "1.7.3" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + shelljs@^0.8.3, shelljs@^0.8.4: version "0.8.4" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" @@ -26734,14 +26809,6 @@ style-inject@^0.3.0: resolved "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3" integrity sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw== -style-loader@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" - integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== - dependencies: - loader-utils "^2.0.0" - schema-utils "^2.6.6" - style-loader@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e" @@ -26750,6 +26817,11 @@ style-loader@^1.3.0: loader-utils "^2.0.0" schema-utils "^2.7.0" +style-loader@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" + integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" @@ -26757,14 +26829,13 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -stylehacks@^4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" - integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== +stylehacks@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb" + integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA== dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" + browserslist "^4.16.0" + postcss-selector-parser "^6.0.4" stylis@^4.0.6: version "4.0.7" @@ -26896,7 +26967,7 @@ svg-parser@^2.0.2: resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -svgo@^1.0.0, svgo@^1.2.2: +svgo@^1.2.2: version "1.3.2" resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== @@ -26915,6 +26986,19 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + swagger-client@3.16.1, swagger-client@^3.16.1: version "3.16.1" resolved "https://registry.npmjs.org/swagger-client/-/swagger-client-3.16.1.tgz#df86c9d407ab52c00cb356e714b0ec732bb3ad40" @@ -27553,11 +27637,6 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -28072,11 +28151,6 @@ uniq@^1.0.1: resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -28616,11 +28690,6 @@ vasync@^2.2.0: dependencies: verror "1.10.0" -vendors@^1.0.0: - version "1.0.4" - resolved "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - verror@1.10.0, verror@^1.8.1: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -28700,6 +28769,11 @@ vm-browserify@^1.0.1: resolved "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vm2@^3.9.5: + version "3.9.5" + resolved "https://registry.npmjs.org/vm2/-/vm2-3.9.5.tgz#5288044860b4bbace443101fcd3bddb2a0aa2496" + integrity sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng== + vscode-languageserver-types@^3.15.1: version "3.15.1" resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" @@ -28804,11 +28878,6 @@ web-streams-polyfill@4.0.0-beta.1: resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz#3b19b9817374b7cee06d374ba7eeb3aeb80e8c95" integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ== -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -29024,14 +29093,6 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - whatwg-url@^8.0.0, whatwg-url@^8.4.0: version "8.4.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" @@ -29269,9 +29330,9 @@ write-pkg@^4.0.0: write-json-file "^3.2.0" ws@7.4.5, ws@^7.4.6: - version "7.5.5" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== + version "7.5.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== ws@7.4.6: version "7.4.6"