diff --git a/.changeset/afraid-trainers-know.md b/.changeset/afraid-trainers-know.md new file mode 100644 index 0000000000..5df3d3eced --- /dev/null +++ b/.changeset/afraid-trainers-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fix a bug preventing an access token to be refreshed a second time with the GitLab provider. diff --git a/.changeset/brave-eggs-rush.md b/.changeset/brave-eggs-rush.md new file mode 100644 index 0000000000..8e1a7ca86d --- /dev/null +++ b/.changeset/brave-eggs-rush.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +--- + +Change the styling of the `` to have more contrast in light +mode. Nodes now have a design similar to material UI buttons. diff --git a/.changeset/cuddly-rabbits-look.md b/.changeset/cuddly-rabbits-look.md new file mode 100644 index 0000000000..421c97bbff --- /dev/null +++ b/.changeset/cuddly-rabbits-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Action needed: If you are using the templates located at https://github.com/backstage/backstage/tree/master/ in your Backstage app directly using the URL via the `app-config.yaml`, you should copy over the templates inside your org and import from there. The templates have now been moved to https://github.com/backstage/software-templates. See https://github.com/backstage/backstage/issues/6415 for explanation. diff --git a/.changeset/fast-actors-grow.md b/.changeset/fast-actors-grow.md new file mode 100644 index 0000000000..c1da84f744 --- /dev/null +++ b/.changeset/fast-actors-grow.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Bump `passport-saml` to version 3. This is a breaking change, in that it [now requires](https://github.com/node-saml/passport-saml/pull/548) the `auth.saml.cert` parameter to be set. If you are not using SAML auth, you can ignore this. + +To update your settings, add something similar to the following to your app-config: + +```yaml +auth: + saml: + # ... other settings ... + cert: 'MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W==' +``` + +For more information, see the [library README](https://github.com/node-saml/passport-saml#security-and-signatures). diff --git a/.changeset/neat-mugs-add.md b/.changeset/neat-mugs-add.md new file mode 100644 index 0000000000..b90c95440d --- /dev/null +++ b/.changeset/neat-mugs-add.md @@ -0,0 +1,7 @@ +--- +'@backstage/integration': patch +--- + +Fix Azure URL handling to properly support both repo shorthand (`/owner/_git/project`) and full URLs (`/owner/project/_git/repo`). + +Fix Azure DevOps Server URL handling by being able to parse URLs with hosts other than `dev.azure.com`. Note that the `api-version` used for API requests is currently `6.0`, meaning you need to support at least this version in your Azure DevOps Server instance. diff --git a/.changeset/polite-dragons-allow.md b/.changeset/polite-dragons-allow.md new file mode 100644 index 0000000000..05eac53a1f --- /dev/null +++ b/.changeset/polite-dragons-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Use a more informative error message if the configured OIDC identity provider does not provide a `userinfo_endpoint` in its metadata. diff --git a/.changeset/real-beans-collect.md b/.changeset/real-beans-collect.md new file mode 100644 index 0000000000..43dae9cd9a --- /dev/null +++ b/.changeset/real-beans-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Fix Azure `readTree` and `search` handling to properly support paths. diff --git a/.changeset/strong-geese-hope.md b/.changeset/strong-geese-hope.md new file mode 100644 index 0000000000..27da35743a --- /dev/null +++ b/.changeset/strong-geese-hope.md @@ -0,0 +1,13 @@ +--- +'@backstage/backend-common': patch +'@backstage/integration': patch +'@backstage/techdocs-common': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs': patch +--- + +Upgrade git-parse-url to v11.6.0 diff --git a/.changeset/techdocs-trains-beg.md b/.changeset/techdocs-trains-beg.md deleted file mode 100644 index 05161b7093..0000000000 --- a/.changeset/techdocs-trains-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Force using `posix` path for cloud storage diff --git a/.changeset/ten-dolls-ring.md b/.changeset/ten-dolls-ring.md new file mode 100644 index 0000000000..8142d1f3c8 --- /dev/null +++ b/.changeset/ten-dolls-ring.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-explore': patch +--- + +Make styling of groups tab in the explore page use the Backstage theme instead +of hard coded colors. Change the layout to be full screen. diff --git a/.changeset/tidy-shoes-occur.md b/.changeset/tidy-shoes-occur.md new file mode 100644 index 0000000000..9ffac60f56 --- /dev/null +++ b/.changeset/tidy-shoes-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Handle pagination on return results when fetch GitHub API to list selected repos diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 781feb1f04..0854635a2b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,18 +8,18 @@ /docs/features/techdocs @backstage/techdocs-core /docs/features/search @backstage/techdocs-core /docs/assets/search @backstage/techdocs-core -/plugins/code-coverage @alde @nissayeva -/plugins/code-coverage-backend @alde @nissayeva +/plugins/code-coverage @backstage/reviewers @alde @nissayeva +/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva /plugins/cost-insights @backstage/silver-lining -/plugins/cloudbuild @trivago/ebarrios +/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios /plugins/search @backstage/techdocs-core /plugins/search-* @backstage/techdocs-core /plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core -/plugins/ilert @yacut +/plugins/ilert @backstage/reviewers @yacut /plugins/home @backstage/techdocs-core /packages/search-common @backstage/techdocs-core /packages/techdocs-common @backstage/techdocs-core -/.changeset/cost-insights-* @backstage/silver-lining +/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining /.changeset/search-* @backstage/techdocs-core /.changeset/techdocs-* @backstage/techdocs-core diff --git a/.github/workflows/snyk-monitor.yml b/.github/workflows/snyk-monitor.yml new file mode 100644 index 0000000000..89a32a4429 --- /dev/null +++ b/.github/workflows/snyk-monitor.yml @@ -0,0 +1,33 @@ +name: Snyk Monitoring + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - '.github/workflows/snyk-monitor.yml' + - '**/.snyk' + - '**/package.json' + - 'yarn.lock' + +# This workflow synchronizes the packages in this repo along with policies in +# each .snyk file with the remote state in our snyk org. It allows us to define +# ignore policies in the .snyk files and then have them show up in the snyk web +# UI, and also automatically adds any new packages that are created. + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Monitor and Synchronize Snyk Policies + uses: snyk/actions/node@master + with: + command: monitor + args: > + --yarn-workspaces + --org=backstage-dgh + --strict-out-of-sync=false + --remote-repo-url=https://github.com/backstage/backstage + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} diff --git a/ADOPTERS.md b/ADOPTERS.md index 16e606c758..18d1388936 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -44,3 +44,4 @@ | [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | | [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | | [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | diff --git a/SECURITY.md b/SECURITY.md index b410404bcf..ba96c694ee 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,3 +26,13 @@ In the event of a vulnerability the runbook for the maintainers is as follows: 1. Create one or multiple Pull Requests with fixes towards the fork. Note that these PRs will not have CI checks run on them, so more care should be taken to run local validation. The PRs are also not merged like normal PRs, but are instead merged straight into the main repo all at once when the merge button is hit on the security advisory. 4. Once the fix is ready in a PR or private fork and it is time to release it, there are a couple of options. Either merge into the main branch with a changeset and wait for a regular release, or do a quick release by manually bumping the version in `package.json` of the affected package, along with a manual `CHANGELOG.md` entry. Note that a quick release will only work if the package does not have any other pending changes that depend on pending changes in other packages, so be sure to manually check that first, and fall back to an early regular release if needed. In general it's best to stick with the regular release flow, with the quick release being used only for time sensitive fixes. 5. Finalize and publish the security advisory. Note that once you hit the publish button it's no longer possible to edit the advisory. Just like the CVE number this can take up to 72h, and expect it to be slower than the CVE number request. + +## Vulnerability Policies + +Backstage uses Snyk vulnerability scans in order to make sure we minimize vulnerabilities in our dependencies and get notified of new vulnerabilities. + +There are many situations where a vulnerability does not affect a particular dependency because of how the vulnerable package is used. In that situation the package authors may choose to stay at the current version rather than bumping the dependency, leading to a warning in the vulnerability scans but no actual vulnerability. + +To work around this and other similar issues, Snyk provides a method to ignore vulnerabilities. In order to provide the best visibility and most utility to adopters of Backstage, we store these ignore rules in `.snyk` policy files. This allows adopters to rely on our ignore policies if they wish to do so. + +Adding a new ignore policy is done by creating or modifying an existing `.snyk` file within a package root. See the [Snyk Documentation](https://support.snyk.io/hc/en-us/articles/360007487097-The-snyk-file) for details on the syntax. Always include a description, full path, and time limit of the ignore policy. diff --git a/app-config.yaml b/app-config.yaml index 9f365becef..2fb75f718e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -133,6 +133,9 @@ kafka: brokers: - localhost:9092 +allure: + baseUrl: http://localhost:5050/allure-docker-service + integrations: github: - host: github.com @@ -309,6 +312,7 @@ auth: saml: entryPoint: 'http://localhost:7001/' issuer: 'passport-saml' + cert: 'fake-cert-base64' okta: development: clientId: ${AUTH_OKTA_CLIENT_ID} diff --git a/docs/assets/features/catalog/life-of-an-entity_ingestion.svg b/docs/assets/features/catalog/life-of-an-entity_ingestion.svg new file mode 100644 index 0000000000..b250744fcd --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_ingestion.svg @@ -0,0 +1,3 @@ + + +
Unprocessed Entities
Unprocessed Entities
change events
change events
Entity Provider
Entity Provider
change events
change events
Entity Provider
Entity Provider
External
Source
External...
External
Source
External...
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Entity
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/features/catalog/life-of-an-entity_overview.svg b/docs/assets/features/catalog/life-of-an-entity_overview.svg new file mode 100644 index 0000000000..3fc35b4775 --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_overview.svg @@ -0,0 +1,3 @@ + + +
Unprocessed Entities
Unprocessed Entities
Processed Entities
Processed Entities
Relations
Relations
Edges
Edges
Final Entities
Final Entities
Search
Search
Errors
Errors
Processors
Processors
Stitcher
Stitcher
Entity Providers
Entity Providers
External
Sources
External...
Catalog
API
Catalog...
Processing
Processing
Stitching
Stitching
Ingestion
Ingestion
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/features/catalog/life-of-an-entity_processing.svg b/docs/assets/features/catalog/life-of-an-entity_processing.svg new file mode 100644 index 0000000000..da3c4c7dba --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_processing.svg @@ -0,0 +1,3 @@ + + +
Processor One
Processor One
Processor Two
Processor Two
Unprocessed Entity
Unprocessed Enti...
emit
e...
preprocess
preprocess

preprocess

preprocess
(intermediate)
(intermediate)
validate
validate
(intermediate)
(intermediate)

postprocess

postprocess
(intermediate)
(intermediate)
Processed
Entity
Processed...


relations,
errors,
other entities
relations,...
emit
e...


relations,
errors,
other entities
relations,...
emit
e...


relations,
errors,
other entities
relations,...
emit
e...


relations,
errors,
other entities
relations,...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/assets/features/catalog/life-of-an-entity_stitching.svg b/docs/assets/features/catalog/life-of-an-entity_stitching.svg new file mode 100644 index 0000000000..3b232a361c --- /dev/null +++ b/docs/assets/features/catalog/life-of-an-entity_stitching.svg @@ -0,0 +1,3 @@ + + +
Processed
Entity
Processed...
Processed
Entity
Processed...
Processed
Entity
Processed...
Final
Entity
Final...
Errors
Errors
Errors
Errors
Errors
Errors
Stitcher
Stitcher
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Relation
Search
Search
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index b64f9b1902..52f463bab4 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -44,7 +44,7 @@ The Microsoft provider is a structure with three configuration keys: - `clientId`: Application (client) ID, found on App Registration > Overview - `clientSecret`: Secret, found on App Registration > Certificates & secrets -- `tenentId`: Directory (tenant) ID, found on App Registration > Overview +- `tenantId`: Directory (tenant) ID, found on App Registration > Overview ## Adding the provider to the Backstage frontend diff --git a/docs/conf/reading.md b/docs/conf/reading.md index 6ac10a8c8b..d492723e3d 100644 --- a/docs/conf/reading.md +++ b/docs/conf/reading.md @@ -112,7 +112,16 @@ example `getString`. These will throw an error if there is no value available. The [ConfigApi](../reference/utility-apis/Config.md) in the frontend is a [UtilityApi](../api/utility-apis.md). It's accessible as usual via the -`configApiRef` exported from `@backstage/core-plugin-api`. +`configApiRef` exported from `@backstage/core-plugin-api`: + +``` +import { useApi, configApiRef } from '@backstage/core-plugin-api'; +... +const MyReactComponent = (...) => { + const config = useApi(configApiRef); + ... +} +``` Depending on the config api in another API is slightly different though, as the `ConfigApi` implementation is supplied via the App itself and not instantiated diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index e542b02ebf..599c01a0ce 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -113,6 +113,9 @@ browser at `http://localhost:7000` ## Multi-stage Build +> NOTE: The `.dockerignore` is different in this setup, read on for more +> details. + This section describes how to set up a multi-stage Docker build that builds the entire project within Docker. This is typically slower than a host build, but is sometimes desired because Docker in Docker is not available in the build @@ -137,6 +140,7 @@ WORKDIR /app COPY package.json yarn.lock ./ COPY packages packages +# Comment this out if you don't have any internal plugins COPY plugins plugins RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ @@ -182,8 +186,9 @@ end up being properly installed. To speed up the build when not running in a fresh clone of the repo you should set up a `.dockerignore`. This one is different than the host build one, because -we want to have access to the source code of all packages for the build, but can -ignore any existing build output or dependencies: +we want to have access to the source code of all packages for the build. We can +however ignore any existing build output or dependencies on the host. For our +new `.dockerignore`, replace the contents of your existing one with this: ```text node_modules diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 07258a55cb..8f855776a0 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -34,15 +34,14 @@ export const CustomCatalogPage = ({ }: CatalogPageProps) => { return ( - - - - All your software catalog entities - - + + + }> + + All your software catalog entities + - - - + + ); }; diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index fb17f4a434..7a9aa099e9 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -245,6 +245,22 @@ the entity belongs to the `"default"` namespace. Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, i.e. not Backstage specific but the same as in Kubernetes. +### `title` [optional] + +A display name of the entity, to be presented in user interfaces instead of the +`name` property above, when available. + +This field is sometimes useful when the `name` is cumbersome or ends up being +perceived as overly technical. The title generally does not have as stringent +format requirements on it, so it may contain special characters and be more +explanatory. Do keep it very short though, and avoid situations where a title +can be confused with the name of another entity, or where two entities share a +title. + +Note that this is only for display purposes, and may be ignored by some parts of +the code. [Entity references](references.md) still always make use of the `name` +property for example, not the title. + ### `description` [optional] A human readable description of the entity, to be shown in Backstage. Should be @@ -531,8 +547,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the component, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +component, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a component is the singular entity (commonly a team) that bears ultimate responsibility for the component, and has the authority and @@ -550,8 +566,8 @@ component, but there will always be one ultimate owner. ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the component -belongs to, e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +component belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -559,8 +575,8 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional. ### `spec.subcomponentOf` [optional] -An [entity reference](#string-references) to another component of which the -component is a part, e.g. `spotify-ios-app`. This field is optional. +An [entity reference](references.md#string-references) to another component of +which the component is a part, e.g. `spotify-ios-app`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -568,8 +584,8 @@ component is a part, e.g. `spotify-ios-app`. This field is optional. ### `spec.providesApis` [optional] -An array of [entity references](#string-references) to the APIs that are -provided by the component, e.g. `artist-api`. This field is optional. +An array of [entity references](references.md#string-references) to the APIs +that are provided by the component, e.g. `artist-api`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | @@ -577,8 +593,8 @@ provided by the component, e.g. `artist-api`. This field is optional. ### `spec.consumesApis` [optional] -An array of [entity references](#string-references) to the APIs that are -consumed by the component, e.g. `artist-api`. This field is optional. +An array of [entity references](references.md#string-references) to the APIs +that are consumed by the component, e.g. `artist-api`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- | @@ -586,9 +602,9 @@ consumed by the component, e.g. `artist-api`. This field is optional. ### `spec.dependsOn` [optional] -An array of [entity references](#string-references) to the components and -resources that the component depends on, e.g. `artists-db`. This field is -optional. +An array of [entity references](references.md#string-references) to the +components and resources that the component depends on, e.g. `artists-db`. This +field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | @@ -690,12 +706,6 @@ shape, this kind has the following structure. Exactly equal to `backstage.io/v1beta2` and `Template`, respectively. -### `metadata.title` [required] - -The nice display name for the template as a string, e.g. `React SSR Template`. -This field is required as is used to reference the template to the user instead -of the `metadata.name` field. - ### `metadata.tags` [optional] A list of strings that can be associated with the template, e.g. @@ -722,8 +732,8 @@ You can find out more about the `steps` key ### `spec.owner` [optional] -An [entity reference](#string-references) to the owner of the template, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +template, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a Template is the singular entity (commonly a team) that bears ultimate responsibility for the Template, and has the authority and @@ -832,8 +842,8 @@ The current set of well-known and common values for this field is: ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the component, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +component, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of an API is the singular entity (commonly a team) that bears ultimate responsibility for the API, and has the authority and capability @@ -851,8 +861,8 @@ one ultimate owner. ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the API belongs to, -e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +API belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1059,8 +1069,8 @@ Exactly equal to `backstage.io/v1alpha1` and `Resource`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the resource, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +resource, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a resource is the singular entity (commonly a team) that bears ultimate responsibility for the resource, and has the authority and @@ -1091,8 +1101,8 @@ Some common values for this field could be: ### `spec.system` [optional] -An [entity reference](#string-references) to the system that the resource -belongs to, e.g. `artist-engagement-portal`. This field is optional. +An [entity reference](references.md#string-references) to the system that the +resource belongs to, e.g. `artist-engagement-portal`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1100,9 +1110,9 @@ belongs to, e.g. `artist-engagement-portal`. This field is optional. ### `spec.dependsOn` [optional] -An array of [entity references](#string-references) to the components and -resources that the resource depends on, e.g. `artist-lookup`. This field is -optional. +An array of [entity references](references.md#string-references) to the +components and resources that the resource depends on, e.g. `artist-lookup`. +This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | @@ -1146,8 +1156,8 @@ Exactly equal to `backstage.io/v1alpha1` and `System`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the system, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +system, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a system is the singular entity (commonly a team) that bears ultimate responsibility for the system, and has the authority and @@ -1165,8 +1175,8 @@ but there will always be one ultimate owner. ### `spec.domain` [optional] -An [entity reference](#string-references) to the domain that the system belongs -to, e.g. `artists`. This field is optional. +An [entity reference](references.md#string-references) to the domain that the +system belongs to, e.g. `artists`. This field is optional. | [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | @@ -1205,8 +1215,8 @@ Exactly equal to `backstage.io/v1alpha1` and `Domain`, respectively. ### `spec.owner` [required] -An [entity reference](#string-references) to the owner of the domain, e.g. -`artist-relations-team`. This field is required. +An [entity reference](references.md#string-references) to the owner of the +domain, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a domain is the singular entity (commonly a team) that bears ultimate responsibility for the domain, and has the authority and diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md new file mode 100644 index 0000000000..9e8a3f0563 --- /dev/null +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -0,0 +1,246 @@ +--- +id: life-of-an-entity +title: The Life of an Entity +sidebar_label: The Life of an Entity +# prettier-ignore +description: The life cycle of entities, from being introduced into the catalog, through processing, to being removed again +--- + +This document gives a high level overview of the catalog backend, and the +technical processes involved in making entities flow through it. It is mainly +aimed at developers who want to understand the internals while installing or +extending the catalog. However, it can be informative for other personas too. + +## Key Concepts + +The catalog forms a hub of sorts, where entities are ingested from various +authoritative sources and held in a database, subject to automated processing, +and then presented through an API for quick and easy access by Backstage and +others. The most common source is [YAML files](descriptor-format.md) on a +standard format, living in version control systems near the source code of +systems that they describe. Those files are registered with the catalog and +maintained by the respective owners. The catalog makes sure to keep itself up to +date with changes to those files. + +The main extension points where developers can customize the catalog are: + +- _Entity providers_, that feed initial raw entity data into the catalog, +- _Policies_, that establish baseline rules about the shape of entities, +- _Processors_, that validate, analyze, and mutate the raw entity data into its + final form. + +The high level processes involved are: + +- _Ingestion_, where entity providers fetch raw entity data from external + sources and seed it into the database, +- _Processing_, where the policies and processors continually treat the ingested + data and may emit both other raw entities (that are also subject to + processing), errors, relations to other entities, etc., +- _Stitching_, where all of the data emitted by various processors are assembled + together into the final output entity. + +An entity is not visible to the outside world (through the catalog API), until +it has passed through the last process and landed among the final entities. + +![General overview](../../assets/features/catalog/life-of-an-entity_overview.svg) + +The details of these processes are described below. + +## Ingestion + +Each catalog deployment has a number of entity providers installed. They are +responsible for fetching data from external authoritative sources in any way +that they see fit, to translate those into entity objects, and to notify the +database when those entities are added or removed. These are the _unprocessed +entities_ that will be subject to later processing (see below), and they form +the very basis of existence for entities. If there were no entity providers, no +entities would ever enter the system. + +The database always keeps track of the set of entities that belong to each +provider; no two providers can try to output the same entity. And when a +provider signals the removal of an entity, then that leads to an _eager +deletion_: the entity and all auxiliary data that it has led to in the database +is immediately purged. + +![Ingestion overview](../../assets/features/catalog/life-of-an-entity_ingestion.svg) + +There are two providers installed by default: the one that deals with user +registered locations (e.g. URLs to YAML files), and the one that deals with +static locations in the app-config. You can add more third party providers by +passing them to the catalog builder in your backend initialization code, and you +can easily write your own. + +An entity provider is a class that implements the `EntityProvider` interface. It +has three main parts: + +- The identity: Each provider instance has a unique, stable identifier that the + database can use to keep track of the originator of each unprocessed entity. +- The connection: During backend startup, each provider is attached to the + catalog runtime. +- The stream of events: During its lifetime, the provider can issue change + events to the runtime at any point in time, to modify its set of unprocessed + entities. + +It is entirely up to the provider to choose how and when it produces these +change events. For example, the app-config provider only fires off an update at +startup and then lies dormant. The location database provider does an initial +update at startup, and then small delta updates every time a location database +change is detected. The LDAP provider is driven externally by a timer loop that +occasionally triggers a full update. Some future provider may be entirely event +driven, feeding off an event bus or web hook. There is no magic coordination +among providers; if they need to arrange synchronization or locking among +themselves for example to avoid duplicate work across multiple catalog service +machines, they need to handle that out-of-band. + +The entities that are emitted get some coarse validation applied to them, to +ensure that they at least adhere to the most basic schema rules about how an +entity should be shaped. For example, they need to have a `kind`, a +`metadata.name`, and optionally a `metadata.namespace`, among others. Apart from +that, the ingestion stage considers its work done, and stores the unprocessed +entities to be picked up at a later time by the processing system. This means +that the more precise validation rules that you put in place on entities are +_not_ yet applied at this stage. + +## Processing + +Every unprocessed entity comes with a timestamp, which tells at what time that +the processing loop should next try to process it. When the entity first +appears, this timestamp is set to "now" - asking for it to be picked up as soon +as possible. + +Each catalog deployment has a number of processors installed. They are +responsible for receiving unprocessed entities that the catalog decided are due +for processing, and then running that data through a number of processing +stages, mutating the entity and emitting auxiliary data about it. When all of +that is done, the catalog takes all of that information and stores it as the +processed entity, and errors and relations to other entities separately. Then, +the catalog checks to see what entities are touched by that output, and triggers +the final assembly of those (see Stitching below). + +![Processing overview](../../assets/features/catalog/life-of-an-entity_processing.svg) + +Entities are always processed one by one, but all of your catalog service hosts +collaborate in doing so to distribute the load. Note how each processor can +contribute to one or more of the fixed steps in the processing pipeline. First +all of the processors' contributions to one step are run in the order that the +processors were registered, then all of their contributions to the next step in +the same order, and so on. + +Each step has the opportunity to optionally modify the entity, and to optionally +emit other information. For example, the processor might look at information in +the `spec` field of the entity, and emit relations that correspond to those +declarations. If the processor emits an entity, then that entity gets stored +verbatim with a timestamp saying that it, too, should be processed as soon as +possible. If errors are emitted, then that signals that something is wrong with +the entity and that it should not replace whatever previously error-free version +we had among the final entities. If relations are emitted, then they are put in +a dedicated relations table to be picked up by the stitching process below. + +> Optional low level detail note: When entities are emitted, the catalog keeps +> track of the edges between the emitting entity and the ones emitted. This +> happens behind the scenes, hidden from the outside, and is used to form a +> graph. This is _not_ the same thing as relations! The purpose of these edges, +> is to be able to detect when an entity becomes orphaned (see below), and to be +> able to perform eager deletions throughout the graph when a root is explicitly +> unregistered and nothing else is keeping lower nodes alive. We will talk more +> about orphaning and deletions later on in this article. + +When the final step has completed, and no errors were encountered, the processed +entity and all of the relations are finally persisted in the database. Then the +catalog considers this entity, and all of the entities it had relations to, +subject for stitching. + +It is worth noting here that the processing does not lead to deletion or +unregistration of entities; it can only call new entities into existence or +update entities that it has previously called into existence. More about that +later. + +## Stitching + +Stitching finalizes the entity, by gathering all of the output from the previous +steps and merging them into the final object which is what is visible from the +catalog API. As the final entity itself gets updated, the stitcher makes sure +that the search table gets refreshed accordingly as well. + +> Note: The search table mentioned here is not related to the core Search +> feature of Backstage. It's rather the table that backs the ability to filter +> catalog API query results. + +![Stitching overview](../../assets/features/catalog/life-of-an-entity_stitching.svg) + +The diagram shows how the stitcher reads from several sources: + +- The processed entity, as returned from the processing step +- The errors, if any, that were emitted by the processing step +- All relations that were emitted by the processing step, as well as any + relations emitted by _other_ entity processing steps that happen to point at + the current entity + +The last part is noteworthy: This is how the stitcher is able to collect all of +the relation edges, both incoming and outgoing, no matter who produced them. + +The stitching is currently a fixed process, that cannot be modified or extended. +This means that any modifications you want to make on the final result, has to +happen during ingestion or processing. + +## Errors + +> TODO: Describe how errors are exposed through entities + +## Orphaning + +As mentioned earlier, entities internally form a graph. The edges go from +processed parent entities, to child entities emitted while processing the +parent. + +The processing loop runs continuously, so these edges are reconsidered over +time. If processing a parent entity no longer emits a given child entity, then +that former edge is severed. If that child has no other edges pointing at it +either, it becomes _orphaned_. The end result is as follows: + +- The stitching process injects a `backstage.io/orphan: 'true'` annotation on + the child entity. +- The child entity is _not_ removed from the catalog, but stays around until + explicitly deleted via the catalog API, or "reclaimed" by the original parent + or another parent starting to reference it. +- The catalog page in Backstage for the child entity detects the new annotation + and informs users about the orphan status. + +Orphaning can occur in several different scenarios. One common cause is that the +end user edited a corresponding catalog catalog-info YAML file removing the +entity's entry. In the case of a `Location` parent entity, orphaning can happen +if removing the target line pointing to the file containing the child entity. +Another common cause is large batch processors such as the ones that crawl +through remote systems looking for entities, no longer finding something that it +used to find before. Maybe the data was moved, or deleted, in the remote system. +So for example when a person leaves the company an LDAP org discovery processor +might leave an orphaned `User` entity behind. Note that this only applies to +processors - ingestion that happens using entity providers work differently, +described below. + +> Note that removing a file, or accidentally corrupting a file so that it cannot +> be read successfully, does _not_ lead to orphaning. Hard errors, including the +> inability to find or read a distinct remote, are marked as such on the entity +> to inform the owner that something is wrong. But processing and other +> behaviors continue as usual. + +The reason that the orphaning mechanism exists instead of having an eager +deletion triggered, is safety. Scenarios like these can happen purely by +accident, due to the asynchronous nature of the system and the fallible nature +of humans. In particular when external systems start consuming and relying on +the catalog, there could be substantial consequences to suddenly dropping +entities without explicit owner consent. The catalog therefore takes the stance +that entities that often were added by direct user action should also be deleted +only by direct user action. + +It is possible to use the catalog API to build automated "reaper" systems that +finally delete entities that are orphaned. This is however not something that's +provided out of the box. + +## Implicit Deletion + +> TODO: Describe the process of entity providers eagerly deleting entities + +## Explicit Deletion + +> TODO: Describe direct deletion via the catalog API diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index bba05fa1ba..b798742463 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -29,7 +29,7 @@ about TechDocs and the philosophy in its - Explore and take advantage of the large ecosystem of [MkDocs plugins](https://www.mkdocs.org/user-guide/plugins/) to create a rich reading experience. -- Search for and find docs (coming soon). +- Search for and find docs. - Highlight text and raise an Issue to create feedback loop to drive quality documentation (future). - Contribute to and deploy from a marketplace of TechDocs widgets (future). diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 5fa14472d1..326ac02969 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -148,12 +148,18 @@ const AppRoutes = () => { > an external storage provider and external CI/CD is used). If you use the > "basic" or "out-of-the-box" setup, you can stop here! No action needed. -The beta version of TechDocs (v0.x.y) made a breaking change to the way TechDocs -content was accessed and stored, allowing pages to be accessed with -case-insensitive entity triplet paths (e.g. `/docs/namespace/kind/name` whereas -in prior versions, they could only be accessed at `/docs/namespace/Kind/name`). -In order to enable this change, documentation has to be stored in an external -storage provider using an object key whose entity triplet is lower-cased. +For the purposes of this guide, TechDocs Beta version is defined as: + +- **TechDocs Plugin**: At least `v0.11.0` +- **TechDocs Backend Plugin**: At least `v0.10.0` +- **TechDocs CLI**: At least `v0.7.0` + +The beta version of TechDocs made a breaking change to the way TechDocs content +was accessed and stored, allowing pages to be accessed with case-insensitive +entity triplet paths (e.g. `/docs/namespace/kind/name` whereas in prior +versions, they could only be accessed at `/docs/namespace/Kind/name`). In order +to enable this change, documentation has to be stored in an external storage +provider using an object key whose entity triplet is lower-cased. New installations of TechDocs since the beta version will work fine with no action, but for those who were running TechDocs prior to this version, a diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 7973a54ab6..862c3c0cb6 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -128,7 +128,7 @@ const themeOptions = createThemeOptions({ tool: genPageTheme(['#123456','#123456'], shapes.round), service: genPageTheme(['#123456','#123456'], shapes.wave), website: genPageTheme(['#123456','#123456'], shapes.wave), - library: genPageTheme(['#123456','#123456'] shapes.wave), + library: genPageTheme(['#123456','#123456'], shapes.wave), other: genPageTheme(['#123456','#123456'], shapes.wave), app: genPageTheme(['#123456','#123456'], shapes.wave), apis: genPageTheme(['#123456','#123456'], shapes.wave), diff --git a/microsite/data/plugins/allure.yaml b/microsite/data/plugins/allure.yaml new file mode 100644 index 0000000000..defa7158c0 --- /dev/null +++ b/microsite/data/plugins/allure.yaml @@ -0,0 +1,9 @@ +--- +title: Allure Reports +author: Deepak Bhardwaj +authorUrl: https://github.com/deepak-bhardwaj-ps +category: Reporting +description: View Allure reports for your components in Backstage. +documentation: https://github.com/backstage/backstage/tree/master/plugins/allure +iconUrl: https://avatars.githubusercontent.com/u/5879127 +npmPackageName: '@backstage/plugin-allure' diff --git a/microsite/data/plugins/aws-cloudformation.yaml b/microsite/data/plugins/aws-cloudformation.yaml new file mode 100644 index 0000000000..7f0feee143 --- /dev/null +++ b/microsite/data/plugins/aws-cloudformation.yaml @@ -0,0 +1,9 @@ +--- +title: AWS CloudFormation +author: Purple Technology +authorUrl: https://blog.purple-technology.com +category: Infrastructure +description: Load Backstage entities from AWS CloudFormation stacks +documentation: https://github.com/purple-technology/backstage-aws-cloudformation-plugin#readme +iconUrl: https://raw.githubusercontent.com/purple-technology/backstage-aws-cloudformation-plugin/master/docs/cloudformation-logo.png +npmPackageName: 'backstage-aws-cloudformation-plugin' diff --git a/microsite/data/plugins/home.yaml b/microsite/data/plugins/home.yaml new file mode 100644 index 0000000000..e37b8c5e03 --- /dev/null +++ b/microsite/data/plugins/home.yaml @@ -0,0 +1,9 @@ +--- +title: Home +author: Spotify +authorUrl: https://github.com/spotify +category: Discovery +description: This plugin provides a composable home page, and ability to create home page components +documentation: https://github.com/backstage/backstage/blob/master/plugins/home/README.md +iconUrl: img/home.png +npmPackageName: '@backstage/plugin-home' diff --git a/microsite/package.json b/microsite/package.json index 532bb62d15..b3048612a7 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -19,7 +19,7 @@ "@spotify/prettier-config": "^11.0.0", "docusaurus": "^2.0.0-alpha.70", "js-yaml": "^4.1.0", - "prettier": "^2.3.2", + "prettier": "^2.4.0", "yarn-lock-check": "^1.0.5" }, "prettier": "@spotify/prettier-config" diff --git a/microsite/sidebars.json b/microsite/sidebars.json index d787fb28aa..d200b615f1 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -36,6 +36,7 @@ "label": "Software Catalog", "ids": [ "features/software-catalog/software-catalog-overview", + "features/software-catalog/life-of-an-entity", "features/software-catalog/configuration", "features/software-catalog/system-model", "features/software-catalog/descriptor-format", diff --git a/microsite/static/img/home.png b/microsite/static/img/home.png new file mode 100644 index 0000000000..e217581586 Binary files /dev/null and b/microsite/static/img/home.png differ diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 0f7ddb9c60..2973f364b0 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -5207,10 +5207,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.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" - integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== +prettier@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.4.0.tgz#85bdfe0f70c3e777cf13a4ffff39713ca6f64cba" + integrity sha512-DsEPLY1dE5HF3BxCRBmD4uYZ+5DCbvatnolqTqcxEgKVZnL2kUfyu7b8pPQ5+hTBkdhU9SLUmK0/pHb07RE4WQ== prismjs@^1.22.0: version "1.23.0" diff --git a/mkdocs.yml b/mkdocs.yml index 73ad52341e..bddce144d3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - Core Features: - Software Catalog: - Overview: 'features/software-catalog/index.md' + - The Life of an Entity: 'features/software-catalog/life-of-an-entity.md' - Catalog Configuration: 'features/software-catalog/configuration.md' - System Model: 'features/software-catalog/system-model.md' - YAML File Format: 'features/software-catalog/descriptor-format.md' diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index a3af287fc8..9b6dc1956a 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,63 @@ # example-app +## 0.2.45 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-todo@0.1.9 + - @backstage/core-components@0.4.1 + - @backstage/catalog-model@0.9.2 + - @backstage/plugin-home@0.3.0 + - @backstage/plugin-scaffolder@0.11.3 + - @backstage/plugin-org@0.3.22 + - @backstage/core-app-api@0.1.12 + - @backstage/plugin-search@0.4.10 + - @backstage/core-plugin-api@0.1.7 + - @backstage/cli@0.7.11 + +## 0.2.44 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@0.11.2 + - @backstage/cli@0.7.10 + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/plugin-techdocs@0.11.1 + - @backstage/core-components@0.4.0 + - @backstage/plugin-catalog-import@0.5.20 + - @backstage/plugin-shortcuts@0.1.7 + - @backstage/plugin-org@0.3.21 + - @backstage/plugin-catalog@0.6.14 + - @backstage/search-common@0.2.0 + - @backstage/plugin-search@0.4.9 + - @backstage/catalog-model@0.9.1 + - @backstage/core-app-api@0.1.11 + - @backstage/integration-react@0.1.8 + - @backstage/plugin-api-docs@0.6.7 + - @backstage/plugin-badges@0.2.8 + - @backstage/plugin-circleci@0.2.22 + - @backstage/plugin-cloudbuild@0.2.22 + - @backstage/plugin-code-coverage@0.1.10 + - @backstage/plugin-cost-insights@0.11.5 + - @backstage/plugin-explore@0.3.15 + - @backstage/plugin-gcp-projects@0.3.3 + - @backstage/plugin-github-actions@0.4.17 + - @backstage/plugin-graphiql@0.2.15 + - @backstage/plugin-home@0.2.1 + - @backstage/plugin-jenkins@0.5.5 + - @backstage/plugin-kafka@0.2.14 + - @backstage/plugin-kubernetes@0.4.12 + - @backstage/plugin-lighthouse@0.2.24 + - @backstage/plugin-newrelic@0.3.3 + - @backstage/plugin-pagerduty@0.3.12 + - @backstage/plugin-rollbar@0.3.13 + - @backstage/plugin-sentry@0.3.20 + - @backstage/plugin-tech-radar@0.4.6 + - @backstage/plugin-todo@0.1.8 + - @backstage/plugin-user-settings@0.3.4 + ## 0.2.43 ### Patch Changes diff --git a/packages/app/cypress/integration/components/search/SearchPage.js b/packages/app/cypress/integration/components/search/SearchPage.js index 4db7acf4fd..6a18d16198 100644 --- a/packages/app/cypress/integration/components/search/SearchPage.js +++ b/packages/app/cypress/integration/components/search/SearchPage.js @@ -34,7 +34,7 @@ describe('SearchPage', () => { cy.visit('/search-next', { onBeforeLoad(win) { cy.stub(win, 'fetch') - .withArgs(`${API_ENDPOINT}?term=&pageCursor=`) + .withArgs(`${API_ENDPOINT}?term=`) .resolves({ ok: true, json: () => ({ results }), @@ -56,7 +56,7 @@ describe('SearchPage', () => { onBeforeLoad(win) { cy.stub(win, 'fetch') .withArgs( - `${API_ENDPOINT}?term=&filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B0%5D=experimental&pageCursor=`, + `${API_ENDPOINT}?term=&filters%5Bkind%5D=Component&filters%5Blifecycle%5D%5B0%5D=experimental`, ) .resolves({ ok: true, @@ -102,7 +102,7 @@ describe('SearchPage', () => { cy.visit('/search-next?query=backstage', { onBeforeLoad(win) { cy.stub(win, 'fetch') - .withArgs(`${API_ENDPOINT}?term=backstage&pageCursor=`) + .withArgs(`${API_ENDPOINT}?term=backstage`) .resolves({ ok: true, json: () => ({ results: [] }), diff --git a/packages/app/package.json b/packages/app/package.json index 162474fd0e..db7a99e632 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,46 +1,46 @@ { "name": "example-app", - "version": "0.2.43", + "version": "0.2.45", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.9.0", - "@backstage/cli": "^0.7.9", - "@backstage/core-app-api": "^0.1.10", - "@backstage/core-components": "^0.3.3", - "@backstage/core-plugin-api": "^0.1.6", - "@backstage/integration-react": "^0.1.7", - "@backstage/plugin-api-docs": "^0.6.6", - "@backstage/plugin-badges": "^0.2.7", - "@backstage/plugin-catalog": "^0.6.13", - "@backstage/plugin-catalog-import": "^0.5.19", - "@backstage/plugin-catalog-react": "^0.4.4", - "@backstage/plugin-circleci": "^0.2.21", - "@backstage/plugin-cloudbuild": "^0.2.21", - "@backstage/plugin-code-coverage": "^0.1.9", - "@backstage/plugin-cost-insights": "^0.11.4", - "@backstage/plugin-explore": "^0.3.14", - "@backstage/plugin-gcp-projects": "^0.3.2", - "@backstage/plugin-github-actions": "^0.4.16", - "@backstage/plugin-graphiql": "^0.2.14", - "@backstage/plugin-home": "^0.2.0", - "@backstage/plugin-jenkins": "^0.5.4", - "@backstage/plugin-kafka": "^0.2.13", - "@backstage/plugin-kubernetes": "^0.4.11", - "@backstage/plugin-lighthouse": "^0.2.23", - "@backstage/plugin-newrelic": "^0.3.2", - "@backstage/plugin-org": "^0.3.20", - "@backstage/plugin-pagerduty": "0.3.11", - "@backstage/plugin-rollbar": "^0.3.12", - "@backstage/plugin-scaffolder": "^0.11.1", - "@backstage/plugin-search": "^0.4.8", - "@backstage/plugin-sentry": "^0.3.19", - "@backstage/plugin-shortcuts": "^0.1.6", - "@backstage/plugin-tech-radar": "^0.4.5", - "@backstage/plugin-techdocs": "^0.11.0", - "@backstage/plugin-todo": "^0.1.7", - "@backstage/plugin-user-settings": "^0.3.3", - "@backstage/search-common": "^0.1.3", + "@backstage/catalog-model": "^0.9.2", + "@backstage/cli": "^0.7.11", + "@backstage/core-app-api": "^0.1.12", + "@backstage/core-components": "^0.4.1", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/integration-react": "^0.1.8", + "@backstage/plugin-api-docs": "^0.6.7", + "@backstage/plugin-badges": "^0.2.8", + "@backstage/plugin-catalog": "^0.6.14", + "@backstage/plugin-catalog-import": "^0.5.20", + "@backstage/plugin-catalog-react": "^0.4.5", + "@backstage/plugin-circleci": "^0.2.22", + "@backstage/plugin-cloudbuild": "^0.2.22", + "@backstage/plugin-code-coverage": "^0.1.10", + "@backstage/plugin-cost-insights": "^0.11.5", + "@backstage/plugin-explore": "^0.3.15", + "@backstage/plugin-gcp-projects": "^0.3.3", + "@backstage/plugin-github-actions": "^0.4.17", + "@backstage/plugin-graphiql": "^0.2.15", + "@backstage/plugin-home": "^0.3.0", + "@backstage/plugin-jenkins": "^0.5.5", + "@backstage/plugin-kafka": "^0.2.14", + "@backstage/plugin-kubernetes": "^0.4.12", + "@backstage/plugin-lighthouse": "^0.2.24", + "@backstage/plugin-newrelic": "^0.3.3", + "@backstage/plugin-org": "^0.3.22", + "@backstage/plugin-pagerduty": "0.3.12", + "@backstage/plugin-rollbar": "^0.3.13", + "@backstage/plugin-scaffolder": "^0.11.3", + "@backstage/plugin-search": "^0.4.10", + "@backstage/plugin-sentry": "^0.3.20", + "@backstage/plugin-shortcuts": "^0.1.7", + "@backstage/plugin-tech-radar": "^0.4.6", + "@backstage/plugin-techdocs": "^0.11.1", + "@backstage/plugin-todo": "^0.1.9", + "@backstage/plugin-user-settings": "^0.3.4", + "@backstage/search-common": "^0.2.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index e171e22ae9..5831b49d2c 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -17,27 +17,28 @@ import React from 'react'; import Grid from '@material-ui/core/Grid'; import { - RandomJokeHomePageComponent, + HomePageRandomJoke, ComponentAccordion, ComponentTabs, ComponentTab, } from '@backstage/plugin-home'; +import { HomePageSearchBar } from '@backstage/plugin-search'; export const HomePage = () => ( - - + + - - + + + + - ( { label: 'Programming', Component: () => ( - @@ -59,7 +60,7 @@ export const HomePage = () => ( { label: 'Any', Component: () => ( - diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index cf4e610b80..54f59d1a6e 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -14,20 +14,19 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core'; -import Pagination from '@material-ui/lab/Pagination'; +import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; import { CatalogResultListItem } from '@backstage/plugin-catalog'; import { + DefaultResultListItem, SearchBar, SearchFilter, SearchResult, + SearchResultPager, SearchType, - DefaultResultListItem, } from '@backstage/plugin-search'; -import { Content, Header, Lifecycle, Page } from '@backstage/core-components'; import { DocsResultListItem } from '@backstage/plugin-techdocs'; -import { SearchResultSet } from '@backstage/search-common'; +import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; +import React from 'react'; const useStyles = makeStyles((theme: Theme) => ({ bar: { @@ -43,57 +42,6 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -// TODO: Move this into the search plugin once pagination is natively supported. -// See: https://github.com/backstage/backstage/issues/6062 -const SearchResultList = ({ results }: SearchResultSet) => { - const pageSize = 10; - const [page, setPage] = useState(1); - const changePage = (_: any, pageIndex: number) => { - setPage(pageIndex); - }; - const pageAmount = Math.ceil((results.length || 0) / pageSize); - return ( - <> - - {results - .slice(pageSize * (page - 1), pageSize * page) - .map(({ type, document }) => { - switch (type) { - case 'software-catalog': - return ( - - ); - case 'techdocs': - return ( - - ); - default: - return ( - - ); - } - })} - - - - ); -}; - const SearchPage = () => { const classes = useStyles(); return ( @@ -127,8 +75,37 @@ const SearchPage = () => { - {({ results }) => } + {({ results }) => ( + + {results.map(({ type, document }) => { + switch (type) { + case 'software-catalog': + return ( + + ); + case 'techdocs': + return ( + + ); + default: + return ( + + ); + } + })} + + )} + diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index f3fcd5a855..7ffd6e52dc 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/backend-common +## 0.9.2 + +### Patch Changes + +- 9e5ed27ec: Properly export all used types. +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config-loader@0.6.8 + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + +## 0.9.1 + +### Patch Changes + +- 714a2a918: Export type that are needed to implement a new `UrlReader` +- Updated dependencies + - @backstage/integration@0.6.3 + ## 0.9.0 ### Minor Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 736899d1ed..eb1f9ad31f 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -9,7 +9,6 @@ import { AzureIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; -import { ConfigReader } from '@backstage/config'; import cors from 'cors'; import Docker from 'dockerode'; import { ErrorRequestHandler } from 'express'; @@ -17,7 +16,6 @@ import express from 'express'; import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegration } from '@backstage/integration'; import { GitLabIntegration } from '@backstage/integration'; -import * as http from 'http'; import { isChildPath } from '@backstage/cli-common'; import { JsonValue } from '@backstage/config'; import { Knex } from 'knex'; @@ -32,8 +30,6 @@ import { Server } from 'http'; import * as winston from 'winston'; import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "AzureUrlReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class AzureUrlReader implements UrlReader { constructor( @@ -42,31 +38,20 @@ export class AzureUrlReader implements UrlReader { treeResponseFactory: ReadTreeResponseFactory; }, ); - // Warning: (ae-forgotten-export) The symbol "ReaderFactory" needs to be exported by the entry point index.d.ts - // // (undocumented) static factory: ReaderFactory; // (undocumented) read(url: string): Promise; - // Warning: (ae-forgotten-export) The symbol "ReadTreeOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) readTree(url: string, options?: ReadTreeOptions): Promise; - // Warning: (ae-forgotten-export) The symbol "ReadUrlOptions" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ReadUrlResponse" needs to be exported by the entry point index.d.ts - // // (undocumented) readUrl(url: string, _options?: ReadUrlOptions): Promise; - // Warning: (ae-forgotten-export) The symbol "SearchOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) search(url: string, options?: SearchOptions): Promise; // (undocumented) toString(): string; } -// Warning: (ae-missing-release-tag) "BitbucketUrlReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class BitbucketUrlReader implements UrlReader { constructor( @@ -89,92 +74,82 @@ export class BitbucketUrlReader implements UrlReader { toString(): string; } -// Warning: (ae-missing-release-tag) "CacheClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface CacheClient { delete(key: string): Promise; get(key: string): Promise; - // Warning: (ae-forgotten-export) The symbol "CacheSetOptions" needs to be exported by the entry point index.d.ts - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + set( + key: string, + value: JsonValue, + options?: CacheClientSetOptions, + ): Promise; } -// Warning: (ae-missing-release-tag) "CacheManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type CacheClientOptions = { + defaultTtl?: number; +}; + +// @public (undocumented) +export type CacheClientSetOptions = { + ttl?: number; +}; + // @public export class CacheManager { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen forPlugin(pluginId: string): PluginCacheManager; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (ae-forgotten-export) The symbol "CacheManagerOptions" needs to be exported by the entry point index.d.ts static fromConfig( config: Config, options?: CacheManagerOptions, ): CacheManager; } -// Warning: (ae-missing-release-tag) "coloredFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type CacheManagerOptions = { + logger?: Logger_2; + onError?: (err: Error) => void; +}; + // @public (undocumented) export const coloredFormat: winston.Logform.Format; -// Warning: (ae-missing-release-tag) "ContainerRunner" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface ContainerRunner { // (undocumented) runContainer(opts: RunContainerOptions): Promise; } -// Warning: (ae-missing-release-tag) "createDatabase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export const createDatabase: typeof createDatabaseClient; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "createDatabaseClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function createDatabaseClient( dbConfig: Config, overrides?: Partial, ): Knex; -// Warning: (ae-missing-release-tag) "createRootLogger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function createRootLogger( options?: winston.LoggerOptions, env?: NodeJS.ProcessEnv, ): winston.Logger; -// Warning: (ae-forgotten-export) The symbol "ServiceBuilderImpl" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createServiceBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; +export function createServiceBuilder(_module: NodeModule): ServiceBuilder; -// Warning: (ae-forgotten-export) The symbol "StatusCheckRouterOptions" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createStatusCheckRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export function createStatusCheckRouter( - options: StatusCheckRouterOptions, -): Promise; +export function createStatusCheckRouter(options: { + logger: Logger_2; + path?: string; + statusCheck?: StatusCheck; +}): Promise; -// Warning: (ae-missing-release-tag) "DatabaseManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class DatabaseManager { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen forPlugin(pluginId: string): PluginDatabaseManager; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static fromConfig(config: Config): DatabaseManager; } -// Warning: (ae-missing-release-tag) "DockerContainerRunner" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { constructor({ dockerClient }: { dockerClient: Docker }); @@ -191,23 +166,17 @@ export class DockerContainerRunner implements ContainerRunner { }: RunContainerOptions): Promise; } -// Warning: (ae-missing-release-tag) "ensureDatabaseExists" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function ensureDatabaseExists( dbConfig: Config, ...databases: Array ): Promise; -// Warning: (ae-missing-release-tag) "errorHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function errorHandler( options?: ErrorHandlerOptions, ): ErrorRequestHandler; -// Warning: (ae-missing-release-tag) "ErrorHandlerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ErrorHandlerOptions = { showStackTraces?: boolean; @@ -215,18 +184,12 @@ export type ErrorHandlerOptions = { logClientErrors?: boolean; }; -// Warning: (ae-missing-release-tag) "getRootLogger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function getRootLogger(): winston.Logger; -// Warning: (ae-missing-release-tag) "getVoidLogger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getVoidLogger(): winston.Logger; -// Warning: (ae-missing-release-tag) "Git" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class Git { // (undocumented) @@ -331,8 +294,6 @@ export class Git { resolveRef({ dir, ref }: { dir: string; ref: string }): Promise; } -// Warning: (ae-missing-release-tag) "GithubUrlReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class GithubUrlReader implements UrlReader { constructor( @@ -356,8 +317,6 @@ export class GithubUrlReader implements UrlReader { toString(): string; } -// Warning: (ae-missing-release-tag) "GitlabUrlReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class GitlabUrlReader implements UrlReader { constructor( @@ -382,41 +341,49 @@ export class GitlabUrlReader implements UrlReader { export { isChildPath }; -// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "loadBackendConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function loadBackendConfig(options: Options): Promise; +export function loadBackendConfig(options: { + logger: Logger_2; + argv: string[]; +}): Promise; -// Warning: (ae-missing-release-tag) "notFoundHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function notFoundHandler(): RequestHandler; -// Warning: (ae-missing-release-tag) "PluginCacheManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type PluginCacheManager = { - getClient: (options?: ClientOptions) => CacheClient; + getClient: (options?: CacheClientOptions) => CacheClient; }; -// Warning: (ae-missing-release-tag) "PluginDatabaseManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface PluginDatabaseManager { getClient(): Promise; } -// Warning: (ae-missing-release-tag) "PluginEndpointDiscovery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type PluginEndpointDiscovery = { getBaseUrl(pluginId: string): Promise; getExternalBaseUrl(pluginId: string): Promise; }; -// Warning: (ae-missing-release-tag) "ReadTreeResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type ReaderFactory = (options: { + config: Config; + logger: Logger_2; + treeResponseFactory: ReadTreeResponseFactory; +}) => UrlReaderPredicateTuple[]; + +// @public +export type ReadTreeOptions = { + filter?( + path: string, + info?: { + size: number; + }, + ): boolean; + etag?: string; +}; + // @public export type ReadTreeResponse = { files(): Promise; @@ -425,41 +392,67 @@ export type ReadTreeResponse = { etag: string; }; -// Warning: (ae-missing-release-tag) "ReadTreeResponseFile" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type ReadTreeResponseDirOptions = { + targetDir?: string; +}; + +// @public (undocumented) +export interface ReadTreeResponseFactory { + // (undocumented) + fromTarArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; + // (undocumented) + fromZipArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; +} + +// @public (undocumented) +export type ReadTreeResponseFactoryOptions = { + stream: Readable; + subpath?: string; + etag: string; + filter?: ( + path: string, + info?: { + size: number; + }, + ) => boolean; +}; + // @public export type ReadTreeResponseFile = { path: string; content(): Promise; }; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "requestLoggingHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type ReadUrlOptions = { + etag?: string; +}; + +// @public +export type ReadUrlResponse = { + buffer(): Promise; + etag?: string; +}; + // @public export function requestLoggingHandler(logger?: Logger_2): RequestHandler; -// Warning: (ae-missing-release-tag) "RequestLoggingHandlerFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type RequestLoggingHandlerFactory = ( logger?: Logger_2, ) => RequestHandler; -// Warning: (ae-missing-release-tag) "resolvePackagePath" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function resolvePackagePath(name: string, ...paths: string[]): string; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "resolveSafeChildPath" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function resolveSafeChildPath(base: string, path: string): string; -// Warning: (ae-missing-release-tag) "RunContainerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type RunContainerOptions = { imageName: string; @@ -472,32 +465,40 @@ export type RunContainerOptions = { pullImage?: boolean; }; -// Warning: (ae-missing-release-tag) "SearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type SearchOptions = { + etag?: string; +}; + // @public export type SearchResponse = { files: SearchResponseFile[]; etag: string; }; -// Warning: (ae-missing-release-tag) "SearchResponseFile" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type SearchResponseFile = { url: string; content(): Promise; }; -// Warning: (ae-missing-release-tag) "ServiceBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ServiceBuilder = { - loadConfig(config: ConfigReader): ServiceBuilder; + loadConfig(config: Config): ServiceBuilder; setPort(port: number): ServiceBuilder; setHost(host: string): ServiceBuilder; setLogger(logger: Logger_2): ServiceBuilder; enableCors(options: cors.CorsOptions): ServiceBuilder; - setHttpsSettings(settings: HttpsSettings): ServiceBuilder; + setHttpsSettings(settings: { + certificate: + | { + key: string; + cert: string; + } + | { + hostname: string; + }; + }): ServiceBuilder; addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; setRequestLoggingHandler( requestLoggingHandler: RequestLoggingHandlerFactory, @@ -505,18 +506,12 @@ export type ServiceBuilder = { start(): Promise; }; -// Warning: (ae-missing-release-tag) "setRootLogger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function setRootLogger(newLogger: winston.Logger): void; -// Warning: (ae-missing-release-tag) "SingleConnectionDatabaseManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export const SingleConnectionDatabaseManager: typeof DatabaseManager; -// Warning: (ae-missing-release-tag) "SingleHostDiscovery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class SingleHostDiscovery implements PluginEndpointDiscovery { static fromConfig( @@ -531,28 +526,19 @@ export class SingleHostDiscovery implements PluginEndpointDiscovery { getExternalBaseUrl(pluginId: string): Promise; } -// Warning: (ae-missing-release-tag) "StatusCheck" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type StatusCheck = () => Promise; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "statusCheckHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function statusCheckHandler( options?: StatusCheckHandlerOptions, ): Promise; -// Warning: (ae-missing-release-tag) "StatusCheckHandlerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface StatusCheckHandlerOptions { statusCheck?: StatusCheck; } -// Warning: (ae-missing-release-tag) "UrlReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type UrlReader = { read(url: string): Promise; @@ -561,49 +547,33 @@ export type UrlReader = { search(url: string, options?: SearchOptions): Promise; }; -// Warning: (ae-missing-release-tag) "UrlReaders" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type UrlReaderPredicateTuple = { + predicate: (url: URL) => boolean; + reader: UrlReader; +}; + // @public export class UrlReaders { - // Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts - static create({ logger, config, factories }: CreateOptions): UrlReader; - static default({ logger, config, factories }: CreateOptions): UrlReader; + static create({ logger, config, factories }: UrlReadersOptions): UrlReader; + static default({ logger, config, factories }: UrlReadersOptions): UrlReader; } -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "useHotCleanup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type UrlReadersOptions = { + config: Config; + logger: Logger_2; + factories?: ReaderFactory[]; +}; + // @public export function useHotCleanup( _module: NodeModule, cancelEffect: () => void, ): void; -// Warning: (tsdoc-undefined-tag) The TSDoc tag "@warning" is not defined in this configuration -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "useHotMemoize" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; -// Warnings were encountered during analysis: -// -// src/cache/types.d.ts:34:5 - (ae-forgotten-export) The symbol "ClientOptions" needs to be exported by the entry point index.d.ts -// src/middleware/errorHandler.d.ts:17:26 - (tsdoc-malformed-html-name) Invalid HTML element: A space is not allowed here -// src/reading/AzureUrlReader.d.ts:9:9 - (ae-forgotten-export) The symbol "ReadTreeResponseFactory" needs to be exported by the entry point index.d.ts -// src/reading/types.d.ts:108:5 - (ae-forgotten-export) The symbol "ReadTreeResponseDirOptions" needs to be exported by the entry point index.d.ts -// src/service/types.d.ts:12:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:22:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:30:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:38:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:47:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:55:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:57:5 - (ae-forgotten-export) The symbol "HttpsSettings" needs to be exported by the entry point index.d.ts -// src/service/types.d.ts:61:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:62:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/service/types.d.ts:70:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 15a559f960..6ec2c8b131 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.0", + "version": "0.9.2", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.2", - "@backstage/config": "^0.1.8", - "@backstage/config-loader": "^0.6.7", - "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.2", + "@backstage/cli-common": "^0.1.3", + "@backstage/config": "^0.1.9", + "@backstage/config-loader": "^0.6.8", + "@backstage/errors": "^0.1.2", + "@backstage/integration": "^0.6.3", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", @@ -49,7 +49,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", - "git-url-parse": "~11.4.4", + "git-url-parse": "^11.6.0", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "keyv": "^4.0.3", @@ -77,7 +77,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.11", "@backstage/test-utils": "^0.1.17", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 860754aae1..79f48a2757 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -22,7 +22,8 @@ type CacheClientArgs = { client: Keyv; }; -type CacheSetOptions = { +/** @public */ +export type CacheClientSetOptions = { /** * Optional TTL in milliseconds. Defaults to the TTL provided when the client * was set up (or no TTL if none are provided). @@ -33,6 +34,8 @@ type CacheSetOptions = { /** * A pre-configured, storage agnostic cache client suitable for use by * Backstage plugins. + * + * @public */ export interface CacheClient { /** @@ -46,7 +49,11 @@ export interface CacheClient { * optional TTL may also be provided, otherwise it defaults to the TTL that * was provided when the client was instantiated. */ - set(key: string, value: JsonValue, options?: CacheSetOptions): Promise; + set( + key: string, + value: JsonValue, + options?: CacheClientSetOptions, + ): Promise; /** * Removes the given key from the cache store. @@ -73,7 +80,7 @@ export class DefaultCacheClient implements CacheClient { async set( key: string, value: JsonValue, - opts: CacheSetOptions = {}, + opts: CacheClientSetOptions = {}, ): Promise { const k = this.getNormalizedKey(key); await this.client.set(k, value, opts.ttl); diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 9d1afdaaeb..d3a1504187 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -22,16 +22,14 @@ import { Logger } from 'winston'; import { getRootLogger } from '../logging'; import { DefaultCacheClient, CacheClient } from './CacheClient'; import { NoStore } from './NoStore'; -import { - CacheManagerOptions, - OptionalOnError, - PluginCacheManager, -} from './types'; +import { CacheManagerOptions, PluginCacheManager } from './types'; /** * Implements a Cache Manager which will automatically create new cache clients * for plugins when requested. All requested cache clients are created with the * connection details provided. + * + * @public */ export class CacheManager { /** @@ -47,13 +45,13 @@ export class CacheManager { private readonly logger: Logger; private readonly store: keyof CacheManager['storeFactories']; private readonly connection: string; - private readonly errorHandler: OptionalOnError; + private readonly errorHandler: CacheManagerOptions['onError']; /** * Creates a new CacheManager instance by reading from the `backend` config * section, specifically the `.cache` key. * - * @param config The loaded application configuration. + * @param config - The loaded application configuration. */ static fromConfig( config: Config, @@ -74,7 +72,7 @@ export class CacheManager { store: string, connectionString: string, logger: Logger, - errorHandler: OptionalOnError, + errorHandler: CacheManagerOptions['onError'], ) { if (!this.storeFactories.hasOwnProperty(store)) { throw new Error(`Unknown cache store: ${store}`); @@ -88,7 +86,7 @@ export class CacheManager { /** * Generates a PluginCacheManager for consumption by plugins. * - * @param pluginId The plugin that the cache manager should be created for. Plugin names should be unique. + * @param pluginId - The plugin that the cache manager should be created for. Plugin names should be unique. */ forPlugin(pluginId: string): PluginCacheManager { return { diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index 45eb431ed1..26e6c102d8 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -14,6 +14,10 @@ * limitations under the License. */ -export type { CacheClient } from './CacheClient'; +export type { CacheClient, CacheClientSetOptions } from './CacheClient'; export { CacheManager } from './CacheManager'; -export type { PluginCacheManager } from './types'; +export type { + PluginCacheManager, + CacheManagerOptions, + CacheClientOptions, +} from './types'; diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index ac5bb91b21..70c46770a8 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -17,7 +17,8 @@ import { Logger } from 'winston'; import { CacheClient } from './CacheClient'; -type ClientOptions = { +/** @public */ +export type CacheClientOptions = { /** * An optional default TTL (in milliseconds) to be set when getting a client * instance. If not provided, data will persist indefinitely by default (or @@ -26,8 +27,7 @@ type ClientOptions = { defaultTtl?: number; }; -export type OptionalOnError = ((err: Error) => void) | undefined; - +/** @public */ export type CacheManagerOptions = { /** * An optional logger for use by the PluginCacheManager. @@ -38,11 +38,13 @@ export type CacheManagerOptions = { * An optional handler for connection errors emitted from the underlying data * store. */ - onError?: OptionalOnError; + onError?: (err: Error) => void; }; /** * The PluginCacheManager manages access to cache stores that Plugins get. + * + * @public */ export type PluginCacheManager = { /** @@ -52,5 +54,5 @@ export type PluginCacheManager = { * stores so that plugins are discouraged from cache-level integration * and/or cache key collisions. */ - getClient: (options?: ClientOptions) => CacheClient; + getClient: (options?: CacheClientOptions) => CacheClient; }; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 7c660ee4e6..96907870cb 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -101,12 +101,6 @@ class ObservableConfigProxy implements Config { } } -type Options = { - logger: Logger; - // process.argv or any other overrides - argv: string[]; -}; - // A global used to ensure that only a single file watcher is active at a time. let currentCancelFunc: () => void; @@ -114,8 +108,14 @@ let currentCancelFunc: () => void; * Load configuration for a Backend. * * This function should only be called once, during the initialization of the backend. + * + * @public */ -export async function loadBackendConfig(options: Options): Promise { +export async function loadBackendConfig(options: { + logger: Logger; + // process.argv or any other overrides + argv: string[]; +}): Promise { const args = parseArgs(options.argv); const configPaths: string[] = [args.config ?? []].flat(); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 2d60104bcb..de39279a4f 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -31,6 +31,7 @@ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } +/** @public */ export class DatabaseManager { /** * Creates a DatabaseManager from `backend.database` config. @@ -40,7 +41,7 @@ export class DatabaseManager { * defaults. Optionally, a user may set `prefix` which is used to prefix generated database * names if config is not provided. * - * @param config The loaded application configuration. + * @param config - The loaded application configuration. */ static fromConfig(config: Config): DatabaseManager { const databaseConfig = config.getConfig('backend.database'); @@ -59,7 +60,7 @@ export class DatabaseManager { /** * Generates a PluginDatabaseManager for consumption by plugins. * - * @param pluginId The plugin that the database manager should be created for. Plugin names + * @param pluginId - The plugin that the database manager should be created for. Plugin names * should be unique as they are used to look up database config overrides under * `backend.database.plugin`. */ diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts index 153aea7f38..a81d60fe83 100644 --- a/packages/backend-common/src/database/SingleConnection.ts +++ b/packages/backend-common/src/database/SingleConnection.ts @@ -22,6 +22,7 @@ import { DatabaseManager } from './DatabaseManager'; * credentials provided; if the database already exists no attempt to create * the database will be made. * + * @public * @deprecated Use `DatabaseManager` from `@backend-common` instead. */ export const SingleConnectionDatabaseManager = DatabaseManager; diff --git a/packages/backend-common/src/database/config.ts b/packages/backend-common/src/database/config.ts index b771811e8c..cf821087d7 100644 --- a/packages/backend-common/src/database/config.ts +++ b/packages/backend-common/src/database/config.ts @@ -19,6 +19,7 @@ import { merge } from 'lodash'; /** * Merges database objects together * + * @public * @param config The base config. The input is not modified * @param overrides Any additional overrides */ diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 6fdb5554bd..8ea46070eb 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -40,8 +40,9 @@ const ConnectorMapping: Record = { /** * Creates a knex database connection * - * @param dbConfig The database config - * @param overrides Additional options to merge with the config + * @public + * @param dbConfig - The database config + * @param overrides - Additional options to merge with the config */ export function createDatabaseClient( dbConfig: Config, @@ -57,12 +58,16 @@ export function createDatabaseClient( /** * Alias for createDatabaseClient + * + * @public * @deprecated Use createDatabaseClient instead */ export const createDatabase = createDatabaseClient; /** * Ensures that the given databases all exist, creating them if they do not. + * + * @public */ export async function ensureDatabaseExists( dbConfig: Config, diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index c1647862af..edcba1e643 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -19,6 +19,8 @@ import { Knex } from 'knex'; /** * The PluginDatabaseManager manages access to databases that Plugins get. + * + * @public */ export interface PluginDatabaseManager { /** diff --git a/packages/backend-common/src/discovery/SingleHostDiscovery.ts b/packages/backend-common/src/discovery/SingleHostDiscovery.ts index 7d19284d9a..c1e092706b 100644 --- a/packages/backend-common/src/discovery/SingleHostDiscovery.ts +++ b/packages/backend-common/src/discovery/SingleHostDiscovery.ts @@ -26,6 +26,8 @@ import { DEFAULT_PORT } from '../service/lib/ServiceBuilderImpl'; * The deployment may be scaled horizontally, as long as the external URL * is the same for all instances. However, internal URLs will always be * resolved to the same host, so there won't be any balancing of internal traffic. + * + * @public */ export class SingleHostDiscovery implements PluginEndpointDiscovery { /** diff --git a/packages/backend-common/src/discovery/types.ts b/packages/backend-common/src/discovery/types.ts index a5915be773..0b502bf88c 100644 --- a/packages/backend-common/src/discovery/types.ts +++ b/packages/backend-common/src/discovery/types.ts @@ -25,6 +25,8 @@ * Implementations of the discovery API can be as simple as a URL pattern * using the pluginId, but could also have overrides for individual plugins, * or query a separate discovery service. + * + * @public */ export type PluginEndpointDiscovery = { /** diff --git a/packages/backend-common/src/hot.ts b/packages/backend-common/src/hot.ts index 951e29da6a..7148778b8c 100644 --- a/packages/backend-common/src/hot.ts +++ b/packages/backend-common/src/hot.ts @@ -46,13 +46,14 @@ function findAllAncestors(_module: NodeModule): NodeModule[] { * * Useful for cleaning intervals, timers, requests etc * + * @public * @example * ```ts * const intervalId = setInterval(doStuff, 1000); * useHotCleanup(module, () => clearInterval(intervalId)); * ``` - * @param _module Reference to the current module where you invoke the fn - * @param cancelEffect Fn that cleans up the ongoing effects + * @param _module - Reference to the current module where you invoke the fn + * @param cancelEffect - Fn that cleans up the ongoing effects */ export function useHotCleanup(_module: NodeModule, cancelEffect: () => void) { if (_module.hot) { @@ -78,16 +79,17 @@ const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key'; * Memoizes a generated value across hot-module reloads. This is useful for * stateful parts of the backend, e.g. to retain a database. * + * @public * @example * ```ts * const db = useHotMemoize(module, () => createDB(dbParams)); * ``` * - * @warning Don't use inside conditionals or loops, + * **NOTE:** Do not use inside conditionals or loops, * same rules as for hooks apply (https://reactjs.org/docs/hooks-rules.html) * - * @param _module Reference to the current module where you invoke the fn - * @param valueFactory Fn that returns the value you want to memoize + * @param _module - Reference to the current module where you invoke the fn + * @param valueFactory - Fn that returns the value you want to memoize */ export function useHotMemoize( _module: NodeModule, diff --git a/packages/backend-common/src/logging/formats.ts b/packages/backend-common/src/logging/formats.ts index 870eeafa2a..0477136e63 100644 --- a/packages/backend-common/src/logging/formats.ts +++ b/packages/backend-common/src/logging/formats.ts @@ -31,6 +31,7 @@ const coloredTemplate = (info: TransformableInfo) => { return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`; }; +/** @public */ export const coloredFormat = winston.format.combine( winston.format.timestamp(), winston.format.colorize({ diff --git a/packages/backend-common/src/logging/rootLogger.ts b/packages/backend-common/src/logging/rootLogger.ts index 58b675d9cf..d06037863c 100644 --- a/packages/backend-common/src/logging/rootLogger.ts +++ b/packages/backend-common/src/logging/rootLogger.ts @@ -21,14 +21,17 @@ import { coloredFormat } from './formats'; let rootLogger: winston.Logger; +/** @public */ export function getRootLogger(): winston.Logger { return rootLogger; } +/** @public */ export function setRootLogger(newLogger: winston.Logger) { rootLogger = newLogger; } +/** @public */ export function createRootLogger( options: winston.LoggerOptions = {}, env = process.env, diff --git a/packages/backend-common/src/logging/voidLogger.ts b/packages/backend-common/src/logging/voidLogger.ts index 0afc1fc8c7..eb9993fe7f 100644 --- a/packages/backend-common/src/logging/voidLogger.ts +++ b/packages/backend-common/src/logging/voidLogger.ts @@ -19,6 +19,8 @@ import * as winston from 'winston'; /** * A logger that just throws away all messages. + * + * @public */ export function getVoidLogger(): winston.Logger { return winston.createLogger({ diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index ee7995f2c6..47c9285b18 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -28,6 +28,7 @@ import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; import { Logger } from 'winston'; import { getRootLogger } from '../logging'; +/** @public */ export type ErrorHandlerOptions = { /** * Whether error response bodies should show error stack traces or not. @@ -44,9 +45,9 @@ export type ErrorHandlerOptions = { logger?: Logger; /** - * Whether any error < 4XX should be logged or not. + * Whether any 4xx errors should be logged or not. * - * If not specified, by default log any 5xx errors. + * If not specified, default to only logging 5xx errors. */ logClientErrors?: boolean; }; @@ -62,6 +63,7 @@ export type ErrorHandlerOptions = { * error types (such as http-error exceptions) and returns the enclosed status * code accordingly. * + * @public * @returns An Express error request handler */ export function errorHandler( diff --git a/packages/backend-common/src/middleware/notFoundHandler.ts b/packages/backend-common/src/middleware/notFoundHandler.ts index 59ca957cc1..53a548c0ad 100644 --- a/packages/backend-common/src/middleware/notFoundHandler.ts +++ b/packages/backend-common/src/middleware/notFoundHandler.ts @@ -22,6 +22,7 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; * Should be used as the very last handler in the chain, as it unconditionally * returns a 404 status. * + * @public * @returns An Express request handler */ export function notFoundHandler(): RequestHandler { diff --git a/packages/backend-common/src/middleware/requestLoggingHandler.ts b/packages/backend-common/src/middleware/requestLoggingHandler.ts index f2f5cbda27..fa114411e1 100644 --- a/packages/backend-common/src/middleware/requestLoggingHandler.ts +++ b/packages/backend-common/src/middleware/requestLoggingHandler.ts @@ -22,7 +22,8 @@ import { getRootLogger } from '../logging'; /** * Logs incoming requests. * - * @param logger An optional logger to use. If not specified, the root logger will be used. + * @public + * @param logger - An optional logger to use. If not specified, the root logger will be used. * @returns An Express request handler */ export function requestLoggingHandler(logger?: Logger): RequestHandler { diff --git a/packages/backend-common/src/middleware/statusCheckHandler.ts b/packages/backend-common/src/middleware/statusCheckHandler.ts index 243d6533c8..a0ba59fac4 100644 --- a/packages/backend-common/src/middleware/statusCheckHandler.ts +++ b/packages/backend-common/src/middleware/statusCheckHandler.ts @@ -16,8 +16,10 @@ import { NextFunction, Request, Response, RequestHandler } from 'express'; +/** @public */ export type StatusCheck = () => Promise; +/** @public */ export interface StatusCheckHandlerOptions { /** * Optional status function which returns a message. @@ -30,7 +32,8 @@ export interface StatusCheckHandlerOptions { * * This is commonly used to implement healthcheck and readiness routes. * - * @param options An optional configuration object. + * @public + * @param options - An optional configuration object. * @returns An Express error request handler */ export async function statusCheckHandler( diff --git a/packages/backend-common/src/paths.ts b/packages/backend-common/src/paths.ts index ffe99fe762..c8a8849d6c 100644 --- a/packages/backend-common/src/paths.ts +++ b/packages/backend-common/src/paths.ts @@ -25,6 +25,8 @@ import { resolve as resolvePath } from 'path'; * This is particularly useful when you want to access assets shipped with * your backend plugin package. When doing so, do not forget to include the assets * in your published package by adding them to `files` in your `package.json`. + * + * @public */ export function resolvePackagePath(name: string, ...paths: string[]) { const req = @@ -40,8 +42,9 @@ export function resolvePackagePath(name: string, ...paths: string[]) { * a path that point to or within the base path. This is useful for resolving * paths from user input, as it otherwise opens up for vulnerabilities. * - * @param base The base directory to resolve the path from. - * @param path The target path, relative or absolute + * @public + * @param base - The base directory to resolve the path from. + * @param path - The target path, relative or absolute * @returns A path that is guaranteed to point to or within the base path. */ export function resolveSafeChildPath(base: string, path: string): string { diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index c7c8853929..e014fa2e75 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -81,14 +81,14 @@ describe('AzureUrlReader', () => { url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', config: createConfig(), response: expect.objectContaining({ - url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master', }), }, { url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', config: createConfig(), response: expect.objectContaining({ - url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', + url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml', }), }, { @@ -125,14 +125,12 @@ describe('AzureUrlReader', () => { { url: 'https://api.com/a/b/blob/master/path/to/c.yaml', config: createConfig(), - error: - 'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + error: 'Azure URL must point to a git repository', }, { url: 'com/a/b/blob/master/path/to/c.yaml', config: createConfig(), - error: - 'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + error: 'Invalid URL: com/a/b/blob/master/path/to/c.yaml', }, { url: '', diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 7b4b1e345e..6015b044bc 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -23,11 +23,9 @@ import { ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; -import parseGitUrl from 'git-url-parse'; import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { stripFirstDirectoryFromPath } from './tree/util'; import { ReadTreeResponseFactory, ReaderFactory, @@ -40,6 +38,7 @@ import { ReadUrlResponse, } from './types'; +/** @public */ export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); @@ -128,28 +127,38 @@ export class AzureUrlReader implements UrlReader { throw new Error(message); } + // When downloading a zip archive from azure on a subpath we get an extra directory + // layer added at the top. With for example the file /a/b/c.txt and a download of + // /a/b, we'll see /b/c.txt in the zip archive. This picks out /b so that we can remove it. + let subpath; + const path = new URL(url).searchParams.get('path'); + if (path) { + subpath = path.split('/').filter(Boolean).slice(-1)[0]; + } + return await this.deps.treeResponseFactory.fromZipArchive({ stream: archiveAzureResponse.body as unknown as Readable, etag: commitSha, filter: options?.filter, + subpath, }); } async search(url: string, options?: SearchOptions): Promise { - const { filepath } = parseGitUrl(url); - const matcher = new Minimatch(filepath); + const treeUrl = new URL(url); + + const path = treeUrl.searchParams.get('path'); + const matcher = path && new Minimatch(path.replace(/^\/+/, '')); // TODO(freben): For now, read the entire repo and filter through that. In // a future improvement, we could be smart and try to deduce that non-glob // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used // to get just that part of the repo. - const treeUrl = new URL(url); treeUrl.searchParams.delete('path'); - treeUrl.pathname = treeUrl.pathname.replace(/\/+$/, ''); const tree = await this.readTree(treeUrl.toString(), { etag: options?.etag, - filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + filter: p => (matcher ? matcher.match(p) : true), }); const files = await tree.files(); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 7925212b20..4965d5a370 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -43,6 +43,8 @@ import { /** * A processor that adds the ability to read files from Bitbucket v1 and v2 APIs, such as * the one exposed by Bitbucket Cloud itself. + * + * @public */ export class BitbucketUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts index 6b45d99cc3..732d3b9f59 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.ts @@ -27,6 +27,8 @@ import { /** * A UrlReader that does a plain fetch of the URL. + * + * @public */ export class FetchUrlReader implements UrlReader { /** diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 5dc1430ea8..1eed5764ef 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -51,6 +51,8 @@ export type GhBlobResponse = /** * A processor that adds the ability to read files from GitHub v3 APIs, such as * the one exposed by GitHub itself. + * + * @public */ export class GithubUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index baee64fab2..b78add6916 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -38,6 +38,7 @@ import { ReadUrlOptions, } from './types'; +/** @public */ export class GitlabUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); diff --git a/packages/backend-common/src/reading/GoogleGcsUrlReader.ts b/packages/backend-common/src/reading/GoogleGcsUrlReader.ts index 9f3fbad302..f1684c2945 100644 --- a/packages/backend-common/src/reading/GoogleGcsUrlReader.ts +++ b/packages/backend-common/src/reading/GoogleGcsUrlReader.ts @@ -48,6 +48,7 @@ const parseURL = ( }; }; +/** @public */ export class GoogleGcsUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, logger }) => { if (!config.has('integrations.googleGcs')) { diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts index f9a4865717..7120a9570f 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -27,7 +27,8 @@ import { FetchUrlReader } from './FetchUrlReader'; import { GoogleGcsUrlReader } from './GoogleGcsUrlReader'; import { AwsS3UrlReader } from './AwsS3UrlReader'; -type CreateOptions = { +/** @public */ +export type UrlReadersOptions = { /** Root config object */ config: Config; /** Logger used by all the readers */ @@ -38,12 +39,14 @@ type CreateOptions = { /** * UrlReaders provide various utilities related to the UrlReader interface. + * + * @public */ export class UrlReaders { /** * Creates a UrlReader without any known types. */ - static create({ logger, config, factories }: CreateOptions): UrlReader { + static create({ logger, config, factories }: UrlReadersOptions): UrlReader { const mux = new UrlReaderPredicateMux(logger); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config, @@ -65,7 +68,7 @@ export class UrlReaders { * * Any additional factories passed will be loaded before the default ones. */ - static default({ logger, config, factories = [] }: CreateOptions) { + static default({ logger, config, factories = [] }: UrlReadersOptions) { return UrlReaders.create({ logger, config, diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 4c601556d7..097f604987 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -19,10 +19,20 @@ export { BitbucketUrlReader } from './BitbucketUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; export type { + ReaderFactory, + ReadTreeOptions, ReadTreeResponse, + ReadTreeResponseFactory, ReadTreeResponseFile, + ReadTreeResponseDirOptions, + ReadTreeResponseFactoryOptions, + ReadUrlOptions, + ReadUrlResponse, + SearchOptions, SearchResponse, SearchResponseFile, UrlReader, + UrlReaderPredicateTuple, } from './types'; export { UrlReaders } from './UrlReaders'; +export type { UrlReadersOptions } from './UrlReaders'; diff --git a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts index 912fddf965..5fdd633102 100644 --- a/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts +++ b/packages/backend-common/src/reading/tree/ReadTreeResponseFactory.ts @@ -18,7 +18,7 @@ import os from 'os'; import { Config } from '@backstage/config'; import { ReadTreeResponse, - FromArchiveOptions, + ReadTreeResponseFactoryOptions, ReadTreeResponseFactory, } from '../types'; import { TarArchiveResponse } from './TarArchiveResponse'; @@ -34,7 +34,9 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory { constructor(private readonly workDir: string) {} - async fromTarArchive(options: FromArchiveOptions): Promise { + async fromTarArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise { return new TarArchiveResponse( options.stream, options.subpath ?? '', @@ -44,7 +46,9 @@ export class DefaultReadTreeResponseFactory implements ReadTreeResponseFactory { ); } - async fromZipArchive(options: FromArchiveOptions): Promise { + async fromZipArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise { return new ZipArchiveResponse( options.stream, options.subpath ?? '', diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts index f6f9a7845a..a54bac0c4a 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts @@ -62,36 +62,6 @@ describe('ZipArchiveResponse', () => { ]); }); - it('should read files and strip root dir if requested', async () => { - const stream = fs.createReadStream('/test-archive-with-extra-root-dir.zip'); - - const res = new ZipArchiveResponse( - stream, - '', - '/tmp', - 'etag', - undefined, - true, - ); - const files = await res.files(); - - expect(files).toEqual([ - { - path: 'mkdocs.yml', - content: expect.any(Function), - }, - { - path: 'docs/index.md', - content: expect.any(Function), - }, - ]); - const contents = await Promise.all(files.map(f => f.content())); - expect(contents.map(c => c.toString('utf8').trim())).toEqual([ - 'site_name: Test', - '# Test', - ]); - }); - it('should read files with filter', async () => { const stream = fs.createReadStream('/test-archive.zip'); diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index fb22f24b94..e07cedde87 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -24,7 +24,6 @@ import { ReadTreeResponseDirOptions, ReadTreeResponseFile, } from '../types'; -import { stripFirstDirectoryFromPath } from './util'; /** * Wraps a zip archive stream into a tree response reader. @@ -38,7 +37,6 @@ export class ZipArchiveResponse implements ReadTreeResponse { private readonly workDir: string, public readonly etag: string, private readonly filter?: (path: string, info: { size: number }) => boolean, - private readonly stripFirstDirectory?: boolean, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -68,12 +66,8 @@ export class ZipArchiveResponse implements ReadTreeResponse { } private shouldBeIncluded(entry: Entry): boolean { - const strippedPath = this.stripFirstDirectory - ? stripFirstDirectoryFromPath(entry.path) - : entry.path; - if (this.subPath) { - if (!strippedPath.startsWith(this.subPath)) { + if (!entry.path.startsWith(this.subPath)) { return false; } } @@ -102,11 +96,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { if (this.shouldBeIncluded(entry)) { files.push({ - path: this.getInnerPath( - this.stripFirstDirectory - ? stripFirstDirectoryFromPath(entry.path) - : entry.path, - ), + path: this.getInnerPath(entry.path), content: () => entry.buffer(), }); } else { @@ -154,11 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { // Ignore directory entries since we handle that with the file entries // as a zip can have files with directories without directory entries if (entry.type === 'File' && this.shouldBeIncluded(entry)) { - const entryPath = this.getInnerPath( - this.stripFirstDirectory - ? stripFirstDirectoryFromPath(entry.path) - : entry.path, - ); + const entryPath = this.getInnerPath(entry.path); const dirname = platformPath.dirname(entryPath); if (dirname) { await fs.mkdirp(platformPath.join(dir, dirname)); diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index 93f287d4fe..b7e1ff823f 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -20,6 +20,8 @@ import { Config } from '@backstage/config'; /** * A generic interface for fetching plain data from URLs. + * + * @public */ export type UrlReader = { /* Used to read a single file and return its content. */ @@ -39,6 +41,7 @@ export type UrlReader = { search(url: string, options?: SearchOptions): Promise; }; +/** @public */ export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; reader: UrlReader; @@ -47,6 +50,8 @@ export type UrlReaderPredicateTuple = { /** * A factory function that can read config to construct zero or more * UrlReaders along with a predicate for when it should be used. + * + * @public */ export type ReaderFactory = (options: { config: Config; @@ -56,6 +61,8 @@ export type ReaderFactory = (options: { /** * An options object for readUrl operations. + * + * @public */ export type ReadUrlOptions = { /** @@ -74,6 +81,8 @@ export type ReadUrlOptions = { /** * A response object for readUrl operations. + * + * @public */ export type ReadUrlResponse = { /** @@ -90,6 +99,8 @@ export type ReadUrlResponse = { /** * An options object for readTree operations. + * + * @public */ export type ReadTreeOptions = { /** @@ -120,8 +131,16 @@ export type ReadTreeOptions = { etag?: string; }; +/** @public */ +export type ReadTreeResponseDirOptions = { + /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ + targetDir?: string; +}; + /** * A response object for readTree operations. + * + * @public */ export type ReadTreeResponse = { /** @@ -142,20 +161,18 @@ export type ReadTreeResponse = { etag: string; }; -export type ReadTreeResponseDirOptions = { - /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ - targetDir?: string; -}; - /** * Represents a single file in a readTree response. + * + * @public */ export type ReadTreeResponseFile = { path: string; content(): Promise; }; -export type FromArchiveOptions = { +/** @public */ +export type ReadTreeResponseFactoryOptions = { // A binary stream of a tar archive. stream: Readable; // If unset, the files at the root of the tree will be read. @@ -167,13 +184,20 @@ export type FromArchiveOptions = { filter?: (path: string, info?: { size: number }) => boolean; }; +/** @public */ export interface ReadTreeResponseFactory { - fromTarArchive(options: FromArchiveOptions): Promise; - fromZipArchive(options: FromArchiveOptions): Promise; + fromTarArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; + fromZipArchive( + options: ReadTreeResponseFactoryOptions, + ): Promise; } /** * An options object for search operations. + * + * @public */ export type SearchOptions = { /** @@ -192,6 +216,8 @@ export type SearchOptions = { /** * The output of a search operation. + * + * @public */ export type SearchResponse = { /** @@ -207,6 +233,8 @@ export type SearchResponse = { /** * Represents a single file in a search response. + * + * @public */ export type SearchResponseFile = { /** diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index e786c279a1..00b103b240 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -31,6 +31,8 @@ From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub Azure 'notempty' token */ + +/** @public */ export class Git { private constructor( private readonly config: { diff --git a/packages/backend-common/src/service/createServiceBuilder.ts b/packages/backend-common/src/service/createServiceBuilder.ts index 17d69e7082..a6ca25415e 100644 --- a/packages/backend-common/src/service/createServiceBuilder.ts +++ b/packages/backend-common/src/service/createServiceBuilder.ts @@ -15,10 +15,13 @@ */ import { ServiceBuilderImpl } from './lib/ServiceBuilderImpl'; +import { ServiceBuilder } from './types'; /** * Creates a new service builder. + * + * @public */ -export function createServiceBuilder(_module: NodeModule) { +export function createServiceBuilder(_module: NodeModule): ServiceBuilder { return new ServiceBuilderImpl(_module); } diff --git a/packages/backend-common/src/service/createStatusCheckRouter.ts b/packages/backend-common/src/service/createStatusCheckRouter.ts index fd794cc9c4..0d0f93f25c 100644 --- a/packages/backend-common/src/service/createStatusCheckRouter.ts +++ b/packages/backend-common/src/service/createStatusCheckRouter.ts @@ -19,7 +19,8 @@ import Router from 'express-promise-router'; import express from 'express'; import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware'; -export interface StatusCheckRouterOptions { +/** @public */ +export async function createStatusCheckRouter(options: { logger: Logger; path?: string; /** @@ -27,11 +28,7 @@ export interface StatusCheckRouterOptions { * Override this to implement your own logic for a health check. */ statusCheck?: StatusCheck; -} - -export async function createStatusCheckRouter( - options: StatusCheckRouterOptions, -): Promise { +}): Promise { const router = Router(); const { path = '/healthcheck', statusCheck } = options; diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index f845397ca2..3765cbcfdd 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; +import { Config } from '@backstage/config'; import cors from 'cors'; import { Router, RequestHandler } from 'express'; import { Server } from 'http'; import { Logger } from 'winston'; -import { HttpsSettings } from './lib/config'; +/** @public */ export type ServiceBuilder = { /** * Sets the service parameters based on configuration. * - * @param config The configuration to read + * @param config - The configuration to read */ - loadConfig(config: ConfigReader): ServiceBuilder; + loadConfig(config: Config): ServiceBuilder; /** * Sets the port to listen on. @@ -36,7 +36,7 @@ export type ServiceBuilder = { * variable named PORT and use that if present, otherwise it picks a default * port (7000). * - * @param port The port to listen on + * @param port - The port to listen on */ setPort(port: number): ServiceBuilder; @@ -45,7 +45,7 @@ export type ServiceBuilder = { * * '' is express default, which listens to all interfaces. * - * @param host The host to listen on + * @param host - The host to listen on */ setHost(host: string): ServiceBuilder; @@ -54,7 +54,7 @@ export type ServiceBuilder = { * * If no logger is given, the default root logger is used. * - * @param logger A winston logger + * @param logger - A winston logger */ setLogger(logger: Logger): ServiceBuilder; @@ -64,7 +64,7 @@ export type ServiceBuilder = { * If this method is not called, the resulting service will not have any * built in CORS handling. * - * @param options Standard CORS options + * @param options - Standard CORS options */ enableCors(options: cors.CorsOptions): ServiceBuilder; @@ -73,15 +73,17 @@ export type ServiceBuilder = { * * If this method is not called, the resulting service will use sensible defaults * - * @param options Standard certificate options + * @param options - Standard certificate options */ - setHttpsSettings(settings: HttpsSettings): ServiceBuilder; + setHttpsSettings(settings: { + certificate: { key: string; cert: string } | { hostname: string }; + }): ServiceBuilder; /** * Adds a router (similar to the express .use call) to the service. * - * @param root The root URL to bind to (e.g. "/api/function1") - * @param router An express router + * @param root - The root URL to bind to (e.g. "/api/function1") + * @param router - An express router */ addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; @@ -90,7 +92,7 @@ export type ServiceBuilder = { * * If no handler is given the default one is used * - * @param requestLoggingHandler a factory function that given a logger returns an handler + * @param requestLoggingHandler - a factory function that given a logger returns an handler */ setRequestLoggingHandler( requestLoggingHandler: RequestLoggingHandlerFactory, @@ -102,4 +104,5 @@ export type ServiceBuilder = { start(): Promise; }; +/** @public */ export type RequestLoggingHandlerFactory = (logger?: Logger) => RequestHandler; diff --git a/packages/backend-common/src/util/ContainerRunner.ts b/packages/backend-common/src/util/ContainerRunner.ts index 8a73be42ba..be861501c3 100644 --- a/packages/backend-common/src/util/ContainerRunner.ts +++ b/packages/backend-common/src/util/ContainerRunner.ts @@ -16,6 +16,7 @@ import { Writable } from 'stream'; +/** @public */ export type RunContainerOptions = { imageName: string; command?: string | string[]; @@ -27,6 +28,7 @@ export type RunContainerOptions = { pullImage?: boolean; }; +/** @public */ export interface ContainerRunner { runContainer(opts: RunContainerOptions): Promise; } diff --git a/packages/backend-common/src/util/DockerContainerRunner.ts b/packages/backend-common/src/util/DockerContainerRunner.ts index 33a4874151..da3ce66ec2 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.ts @@ -23,6 +23,7 @@ export type UserOptions = { User?: string; }; +/** @public */ export class DockerContainerRunner implements ContainerRunner { private readonly dockerClient: Docker; diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 6d33537e7c..6e10d188df 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/backend-test-utils +## 0.1.7 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/config@0.1.9 + - @backstage/backend-common@0.9.2 + - @backstage/cli@0.7.11 + ## 0.1.6 ### Patch Changes diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 411ecc7ddf..184ed74978 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -5,13 +5,9 @@ ```ts import { Knex } from 'knex'; -// Warning: (ae-missing-release-tag) "isDockerDisabledForTests" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function isDockerDisabledForTests(): boolean; -// Warning: (ae-missing-release-tag) "TestDatabaseId" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type TestDatabaseId = | 'POSTGRES_13' @@ -19,8 +15,6 @@ export type TestDatabaseId = | 'MYSQL_8' | 'SQLITE_3'; -// Warning: (ae-missing-release-tag) "TestDatabases" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class TestDatabases { static create(options?: { @@ -29,7 +23,6 @@ export class TestDatabases { }): TestDatabases; // (undocumented) eachSupportedId(): [TestDatabaseId][]; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen init(id: TestDatabaseId): Promise; // (undocumented) supports(id: TestDatabaseId): boolean; diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index cd2855b833..1713f05d25 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/cli": "^0.7.8", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.2", + "@backstage/cli": "^0.7.11", + "@backstage/config": "^0.1.9", "knex": "^0.95.1", "mysql2": "^2.2.5", "pg": "^8.3.0", @@ -41,7 +41,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.11", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend-test-utils/src/database/TestDatabases.ts b/packages/backend-test-utils/src/database/TestDatabases.ts index 329aec5e8a..243c6bbe52 100644 --- a/packages/backend-test-utils/src/database/TestDatabases.ts +++ b/packages/backend-test-utils/src/database/TestDatabases.ts @@ -31,6 +31,8 @@ import { /** * Encapsulates the creation of ephemeral test database instances for use * inside unit or integration tests. + * + * @public */ export class TestDatabases { private readonly instanceById: Map; @@ -113,7 +115,7 @@ export class TestDatabases { * Returns a fresh, unique, empty logical database on an instance of the * given database ID platform. * - * @param id The ID of the database platform to use, e.g. 'POSTGRES_13' + * @param id - The ID of the database platform to use, e.g. 'POSTGRES_13' * @returns A `Knex` connection object */ async init(id: TestDatabaseId): Promise { diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts index 514a3a85e8..d405f73738 100644 --- a/packages/backend-test-utils/src/database/types.ts +++ b/packages/backend-test-utils/src/database/types.ts @@ -19,6 +19,8 @@ import { Knex } from 'knex'; /** * The possible databases to test against. + * + * @public */ export type TestDatabaseId = | 'POSTGRES_13' diff --git a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts index 617e9eb2a1..b411086728 100644 --- a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts +++ b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ export function isDockerDisabledForTests() { // If we are not running in continuous integration, the default is to skip // the (relatively heavy, long running) docker based tests. If you want to diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index b27dbd07e7..9ca401a105 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,23 @@ # example-backend +## 0.2.44 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.13.6 + - @backstage/plugin-scaffolder-backend@0.15.3 + - @backstage/plugin-techdocs-backend@0.10.1 + - @backstage/plugin-auth-backend@0.3.24 + - @backstage/integration@0.6.3 + - @backstage/plugin-search-backend@0.2.6 + - @backstage/plugin-search-backend-module-elasticsearch@0.0.3 + - @backstage/plugin-search-backend-module-pg@0.2.0 + - @backstage/plugin-search-backend-node@0.4.2 + - @backstage/catalog-model@0.9.1 + - @backstage/backend-common@0.9.1 + - example-app@0.2.44 + ## 0.2.43 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 763d891f77..56b73e0591 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.43", + "version": "0.2.44", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,15 +27,15 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", + "@backstage/backend-common": "^0.9.1", "@backstage/catalog-client": "^0.3.17", - "@backstage/catalog-model": "^0.9.0", + "@backstage/catalog-model": "^0.9.1", "@backstage/config": "^0.1.8", - "@backstage/integration": "^0.6.2", + "@backstage/integration": "^0.6.3", "@backstage/plugin-app-backend": "^0.3.16", - "@backstage/plugin-auth-backend": "^0.3.23", + "@backstage/plugin-auth-backend": "^0.3.24", "@backstage/plugin-badges-backend": "^0.1.9", - "@backstage/plugin-catalog-backend": "^0.13.5", + "@backstage/plugin-catalog-backend": "^0.13.6", "@backstage/plugin-code-coverage-backend": "^0.1.10", "@backstage/plugin-graphql-backend": "^0.1.9", "@backstage/plugin-jenkins-backend": "^0.1.4", @@ -43,19 +43,19 @@ "@backstage/plugin-kafka-backend": "^0.2.9", "@backstage/plugin-proxy-backend": "^0.2.12", "@backstage/plugin-rollbar-backend": "^0.1.14", - "@backstage/plugin-scaffolder-backend": "^0.15.2", + "@backstage/plugin-scaffolder-backend": "^0.15.3", "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", - "@backstage/plugin-search-backend": "^0.2.5", - "@backstage/plugin-search-backend-node": "^0.4.0", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.2", - "@backstage/plugin-search-backend-module-pg": "^0.1.3", - "@backstage/plugin-techdocs-backend": "^0.10.0", + "@backstage/plugin-search-backend": "^0.2.6", + "@backstage/plugin-search-backend-node": "^0.4.2", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.3", + "@backstage/plugin-search-backend-module-pg": "^0.2.0", + "@backstage/plugin-techdocs-backend": "^0.10.1", "@backstage/plugin-todo-backend": "^0.1.11", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", - "azure-devops-node-api": "^10.2.2", + "azure-devops-node-api": "^11.0.1", "dockerode": "^3.2.1", - "example-app": "^0.2.43", + "example-app": "^0.2.44", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", @@ -67,7 +67,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.10", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 3470110408..2da108608e 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -68,7 +68,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { discovery }), }); indexBuilder.addCollator({ diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index d04d1dc936..b32007a3e2 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/catalog-client +## 0.3.19 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/catalog-model@0.9.2 + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + ## 0.3.18 ### Patch Changes diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 173af9aef3..f059174ba3 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -7,8 +7,6 @@ import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { Location as Location_2 } from '@backstage/catalog-model'; -// Warning: (ae-missing-release-tag) "AddLocationRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AddLocationRequest = { type?: string; @@ -17,21 +15,15 @@ export type AddLocationRequest = { presence?: 'optional' | 'required'; }; -// Warning: (ae-missing-release-tag) "AddLocationResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type AddLocationResponse = { location: Location_2; entities: Entity[]; }; -// Warning: (ae-missing-release-tag) "CATALOG_FILTER_EXISTS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const CATALOG_FILTER_EXISTS: unique symbol; -// Warning: (ae-missing-release-tag) "CatalogApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface CatalogApi { // (undocumented) @@ -76,8 +68,6 @@ export interface CatalogApi { ): Promise; } -// Warning: (ae-missing-release-tag) "CatalogClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class CatalogClient implements CatalogApi { constructor(options: { discoveryApi: DiscoveryApi }); @@ -123,8 +113,6 @@ export class CatalogClient implements CatalogApi { ): Promise; } -// Warning: (ae-missing-release-tag) "CatalogEntitiesRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogEntitiesRequest = { filter?: @@ -134,29 +122,24 @@ export type CatalogEntitiesRequest = { fields?: string[] | undefined; }; -// Warning: (ae-missing-release-tag) "CatalogListResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogListResponse = { items: T[]; }; -// Warning: (ae-missing-release-tag) "CatalogRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type CatalogRequestOptions = { token?: string; }; -// Warning: (ae-missing-release-tag) "ENTITY_STATUS_CATALOG_PROCESSING_TYPE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; + // @public export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; -// Warnings were encountered during analysis: -// -// src/CatalogClient.d.ts:7:9 - (ae-forgotten-export) The symbol "DiscoveryApi" needs to be exported by the entry point index.d.ts - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index cdbee32305..2040090fde 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-client", - "version": "0.3.18", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,13 +29,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.5", - "@backstage/errors": "^0.1.1", + "@backstage/catalog-model": "^0.9.2", + "@backstage/config": "^0.1.9", + "@backstage/errors": "^0.1.2", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.7.6", + "@backstage/cli": "^0.7.11", "@types/jest": "^26.0.7", "msw": "^0.29.0" }, diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index ef90287c37..410d921d3b 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -36,6 +36,7 @@ import { } from './types/api'; import { DiscoveryApi } from './types/discovery'; +/** @public */ export class CatalogClient implements CatalogApi { private readonly discoveryApi: DiscoveryApi; diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index d30caa50d6..e78bd9d713 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -16,8 +16,10 @@ import { Entity, EntityName, Location } from '@backstage/catalog-model'; +/** @public */ export const CATALOG_FILTER_EXISTS = Symbol('CATALOG_FILTER_EXISTS'); +/** @public */ export type CatalogEntitiesRequest = { filter?: | Record[] @@ -26,14 +28,17 @@ export type CatalogEntitiesRequest = { fields?: string[] | undefined; }; +/** @public */ export type CatalogListResponse = { items: T[]; }; +/** @public */ export type CatalogRequestOptions = { token?: string; }; +/** @public */ export interface CatalogApi { // Entities getEntities( @@ -72,6 +77,7 @@ export interface CatalogApi { ): Promise; } +/** @public */ export type AddLocationRequest = { type?: string; target: string; @@ -79,6 +85,7 @@ export type AddLocationRequest = { presence?: 'optional' | 'required'; }; +/** @public */ export type AddLocationResponse = { location: Location; entities: Entity[]; diff --git a/packages/catalog-client/src/types/discovery.ts b/packages/catalog-client/src/types/discovery.ts index 447998b3b8..19ee5ed19c 100644 --- a/packages/catalog-client/src/types/discovery.ts +++ b/packages/catalog-client/src/types/discovery.ts @@ -16,6 +16,8 @@ /** * This is a copy of the core DiscoveryApi, to avoid importing core. + * + * @public */ export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 3bfcdd7fba..7963891a4c 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -22,5 +22,6 @@ export type { CatalogListResponse, CatalogRequestOptions, } from './api'; +export type { DiscoveryApi } from './discovery'; export { CATALOG_FILTER_EXISTS } from './api'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; diff --git a/packages/catalog-client/src/types/status.ts b/packages/catalog-client/src/types/status.ts index d2935b890e..38560a3791 100644 --- a/packages/catalog-client/src/types/status.ts +++ b/packages/catalog-client/src/types/status.ts @@ -17,6 +17,8 @@ /** * The entity `status.items[].type` for the status of the processing engine in * regards to an entity. + * + * @public */ export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 5dbbfd241f..6e6a7193c1 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/catalog-model +## 0.9.2 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + +## 0.9.1 + +### Patch Changes + +- 13dc3735c: Add an optional `metadata.title` field to all entity kinds. + + This used to be available on only the `Template` kind, and we have decided that the metadata block should be the same for all kinds. A title can be useful especially in large and complex catalogs where users have a tough time navigating or discerning among the entities. + + It also carries some risk. You do not want to end up giving a title that collides with an actual name, which at best leads to confusion and at worst could be a liability. We do not perform any collision detection in the catalog. If you want to disallow this facility you may want to add a small processor that makes sure it's not set. + + At the time of writing this message, only the scaffolder actually makes use of this field for display purposes. + ## 0.9.0 ### Minor Changes diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 13041d9e32..407e18a409 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -9,9 +9,6 @@ import { JsonValue } from '@backstage/config'; import { SerializedError } from '@backstage/errors'; import * as yup from 'yup'; -// Warning: (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative -// Warning: (ae-missing-release-tag) "analyzeLocationSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const analyzeLocationSchema: yup.ObjectSchema< { @@ -20,8 +17,6 @@ export const analyzeLocationSchema: yup.ObjectSchema< object >; -// Warning: (ae-missing-release-tag) "ApiEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface ApiEntityV1alpha1 extends Entity { // (undocumented) @@ -40,43 +35,24 @@ interface ApiEntityV1alpha1 extends Entity { export { ApiEntityV1alpha1 as ApiEntity }; export { ApiEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "apiEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const apiEntityV1alpha1Validator: KindValidator; -// Warning: (ae-missing-release-tag) "CommonValidatorFunctions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class CommonValidatorFunctions { - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static isJsonSafe(value: unknown): boolean; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static isValidDnsLabel(value: unknown): boolean; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static isValidDnsSubdomain(value: unknown): boolean; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static isValidPrefixAndOrSuffix( value: unknown, separator: string, isValidPrefix: (value: string) => boolean, isValidSuffix: (value: string) => boolean, ): boolean; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static isValidString(value: unknown): boolean; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static isValidUrl(value: unknown): boolean; } -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-forgotten-export) The symbol "EntityRefContext" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "compareEntityToRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function compareEntityToRef( entity: Entity, @@ -84,8 +60,6 @@ export function compareEntityToRef( context?: EntityRefContext, ): boolean; -// Warning: (ae-missing-release-tag) "ComponentEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface ComponentEntityV1alpha1 extends Entity { // (undocumented) @@ -107,13 +81,9 @@ interface ComponentEntityV1alpha1 extends Entity { export { ComponentEntityV1alpha1 as ComponentEntity }; export { ComponentEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "componentEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const componentEntityV1alpha1Validator: KindValidator; -// Warning: (ae-missing-release-tag) "DefaultNamespaceEntityPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class DefaultNamespaceEntityPolicy implements EntityPolicy { constructor(namespace?: string); @@ -121,8 +91,6 @@ export class DefaultNamespaceEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// Warning: (ae-missing-release-tag) "DomainEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface DomainEntityV1alpha1 extends Entity { // (undocumented) @@ -137,18 +105,12 @@ interface DomainEntityV1alpha1 extends Entity { export { DomainEntityV1alpha1 as DomainEntity }; export { DomainEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "domainEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const domainEntityV1alpha1Validator: KindValidator; -// Warning: (ae-missing-release-tag) "EDIT_URL_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; -// Warning: (ae-missing-release-tag) "Entity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type Entity = { apiVersion: string; @@ -159,13 +121,9 @@ export type Entity = { status?: UNSTABLE_EntityStatus; }; -// Warning: (ae-missing-release-tag) "ENTITY_DEFAULT_NAMESPACE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const ENTITY_DEFAULT_NAMESPACE = 'default'; -// Warning: (ae-missing-release-tag) "ENTITY_META_GENERATED_FIELDS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const ENTITY_META_GENERATED_FIELDS: readonly [ 'uid', @@ -173,8 +131,6 @@ export const ENTITY_META_GENERATED_FIELDS: readonly [ 'generation', ]; -// Warning: (ae-missing-release-tag) "EntityEnvelope" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityEnvelope = { apiVersion: string; @@ -185,35 +141,19 @@ export type EntityEnvelope = { }; }; -// Warning: (ae-missing-release-tag) "entityEnvelopeSchemaValidator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/catalog-model" does not have an export "TypeError" -// // @public export function entityEnvelopeSchemaValidator< T extends EntityEnvelope = EntityEnvelope, >(schema?: unknown): (data: unknown) => T; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "entityHasChanges" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function entityHasChanges(previous: Entity, next: Entity): boolean; -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (ae-missing-release-tag) "entityKindSchemaValidator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/catalog-model" does not have an export "TypeError" -// // @public export function entityKindSchemaValidator( schema: unknown, ): (data: unknown) => T | false; -// Warning: (ae-missing-release-tag) "EntityLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityLink = { url: string; @@ -221,8 +161,6 @@ export type EntityLink = { icon?: string; }; -// Warning: (ae-missing-release-tag) "EntityMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityMeta = JsonObject & { uid?: string; @@ -230,6 +168,7 @@ export type EntityMeta = JsonObject & { generation?: number; name: string; namespace?: string; + title?: string; description?: string; labels?: Record; annotations?: Record; @@ -237,8 +176,6 @@ export type EntityMeta = JsonObject & { links?: EntityLink[]; }; -// Warning: (ae-missing-release-tag) "EntityName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityName = { kind: string; @@ -246,23 +183,17 @@ export type EntityName = { name: string; }; -// Warning: (ae-missing-release-tag) "EntityPolicies" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityPolicies: { - allOf(policies: EntityPolicy[]): AllEntityPolicies; - oneOf(policies: EntityPolicy[]): AnyEntityPolicy; + allOf(policies: EntityPolicy[]): EntityPolicy; + oneOf(policies: EntityPolicy[]): EntityPolicy; }; -// Warning: (ae-missing-release-tag) "EntityPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityPolicy = { enforce(entity: Entity): Promise; }; -// Warning: (ae-missing-release-tag) "EntityRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityRef = | string @@ -272,16 +203,18 @@ export type EntityRef = name: string; }; -// Warning: (ae-missing-release-tag) "EntityRelation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export type EntityRefContext = { + defaultKind?: string; + defaultNamespace?: string; +}; + // @public export type EntityRelation = { type: string; target: EntityName; }; -// Warning: (ae-missing-release-tag) "EntityRelationSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityRelationSpec = { source: EntityName; @@ -289,16 +222,11 @@ export type EntityRelationSpec = { target: EntityName; }; -// Warning: (ae-missing-release-tag) "entitySchemaValidator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/catalog-model" does not have an export "TypeError" -// // @public export function entitySchemaValidator( schema?: unknown, ): (data: unknown) => T; -// Warning: (ae-missing-release-tag) "FieldFormatEntityPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class FieldFormatEntityPolicy implements EntityPolicy { constructor(validators?: Validators); @@ -306,40 +234,24 @@ export class FieldFormatEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// Warning: (ae-missing-release-tag) "generateEntityEtag" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function generateEntityEtag(): string; -// Warning: (ae-missing-release-tag) "generateEntityUid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function generateEntityUid(): string; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "generateUpdatedEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function generateUpdatedEntity(previous: Entity, next: Entity): Entity; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "getEntityName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getEntityName(entity: Entity): EntityName; -// Warning: (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" -// Warning: (ae-missing-release-tag) "getEntitySourceLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function getEntitySourceLocation(entity: Entity): { type: string; target: string; }; -// Warning: (ae-missing-release-tag) "GroupEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface GroupEntityV1alpha1 extends Entity { // (undocumented) @@ -362,28 +274,20 @@ interface GroupEntityV1alpha1 extends Entity { export { GroupEntityV1alpha1 as GroupEntity }; export { GroupEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "groupEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const groupEntityV1alpha1Validator: KindValidator; -// Warning: (ae-missing-release-tag) "JSONSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue; }; -// Warning: (ae-missing-release-tag) "KindValidator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type KindValidator = { check(entity: Entity): Promise; }; -// Warning: (ae-missing-release-tag) "KubernetesValidatorFunctions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class KubernetesValidatorFunctions { // (undocumented) @@ -404,21 +308,15 @@ export class KubernetesValidatorFunctions { static isValidObjectName(value: unknown): boolean; } -// Warning: (ae-missing-release-tag) "Location" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) type Location_2 = { id: string; } & LocationSpec; export { Location_2 as Location }; -// Warning: (ae-missing-release-tag) "LOCATION_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; -// Warning: (ae-missing-release-tag) "LocationEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface LocationEntityV1alpha1 extends Entity { // (undocumented) @@ -435,19 +333,12 @@ interface LocationEntityV1alpha1 extends Entity { export { LocationEntityV1alpha1 as LocationEntity }; export { LocationEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "locationEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const locationEntityV1alpha1Validator: KindValidator; -// Warning: (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative -// Warning: (ae-missing-release-tag) "locationSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const locationSchema: yup.ObjectSchema; -// Warning: (ae-missing-release-tag) "LocationSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type LocationSpec = { type: string; @@ -455,19 +346,12 @@ export type LocationSpec = { presence?: 'optional' | 'required'; }; -// Warning: (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative -// Warning: (ae-missing-release-tag) "locationSpecSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated (undocumented) export const locationSpecSchema: yup.ObjectSchema; -// Warning: (ae-missing-release-tag) "makeValidator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export function makeValidator(overrides?: Partial): Validators; -// Warning: (ae-missing-release-tag) "NoForeignRootFieldsEntityPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { constructor(knownFields?: string[]); @@ -475,28 +359,16 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// Warning: (ae-missing-release-tag) "ORIGIN_LOCATION_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "parseEntityName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function parseEntityName( ref: EntityRef, context?: EntityRefContext, ): EntityName; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "parseEntityRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-missing-release-tag) "parseEntityRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-missing-release-tag) "parseEntityRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function parseEntityRef( ref: EntityRef, @@ -534,89 +406,54 @@ export function parseEntityRef( name: string; }; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (ae-missing-release-tag) "parseLocationReference" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function parseLocationReference(ref: string): { type: string; target: string; }; -// Warning: (ae-missing-release-tag) "RELATION_API_CONSUMED_BY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; -// Warning: (ae-missing-release-tag) "RELATION_API_PROVIDED_BY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_API_PROVIDED_BY = 'apiProvidedBy'; -// Warning: (ae-missing-release-tag) "RELATION_CHILD_OF" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_CHILD_OF = 'childOf'; -// Warning: (ae-missing-release-tag) "RELATION_CONSUMES_API" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const RELATION_CONSUMES_API = 'consumesApi'; -// Warning: (ae-missing-release-tag) "RELATION_DEPENDENCY_OF" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_DEPENDENCY_OF = 'dependencyOf'; -// Warning: (ae-missing-release-tag) "RELATION_DEPENDS_ON" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const RELATION_DEPENDS_ON = 'dependsOn'; -// Warning: (ae-missing-release-tag) "RELATION_HAS_MEMBER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_HAS_MEMBER = 'hasMember'; -// Warning: (ae-missing-release-tag) "RELATION_HAS_PART" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_HAS_PART = 'hasPart'; -// Warning: (ae-missing-release-tag) "RELATION_MEMBER_OF" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const RELATION_MEMBER_OF = 'memberOf'; -// Warning: (ae-missing-release-tag) "RELATION_OWNED_BY" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const RELATION_OWNED_BY = 'ownedBy'; -// Warning: (ae-missing-release-tag) "RELATION_OWNER_OF" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_OWNER_OF = 'ownerOf'; -// Warning: (ae-missing-release-tag) "RELATION_PARENT_OF" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const RELATION_PARENT_OF = 'parentOf'; -// Warning: (ae-missing-release-tag) "RELATION_PART_OF" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const RELATION_PART_OF = 'partOf'; -// Warning: (ae-missing-release-tag) "RELATION_PROVIDES_API" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const RELATION_PROVIDES_API = 'providesApi'; -// Warning: (ae-missing-release-tag) "ResourceEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface ResourceEntityV1alpha1 extends Entity { // (undocumented) @@ -634,22 +471,15 @@ interface ResourceEntityV1alpha1 extends Entity { export { ResourceEntityV1alpha1 as ResourceEntity }; export { ResourceEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "resourceEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const resourceEntityV1alpha1Validator: KindValidator; -// Warning: (ae-missing-release-tag) "SchemaValidEntityPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class SchemaValidEntityPolicy implements EntityPolicy { // (undocumented) enforce(entity: Entity): Promise; } -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "serializeEntityRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public @deprecated export function serializeEntityRef( ref: @@ -661,14 +491,9 @@ export function serializeEntityRef( }, ): EntityRef; -// Warning: (ae-missing-release-tag) "SOURCE_LOCATION_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "stringifyEntityRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function stringifyEntityRef( ref: @@ -680,19 +505,12 @@ export function stringifyEntityRef( }, ): string; -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "stringifyLocationReference" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function stringifyLocationReference(ref: { type: string; target: string; }): string; -// Warning: (ae-missing-release-tag) "SystemEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface SystemEntityV1alpha1 extends Entity { // (undocumented) @@ -708,13 +526,9 @@ interface SystemEntityV1alpha1 extends Entity { export { SystemEntityV1alpha1 as SystemEntity }; export { SystemEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "systemEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const systemEntityV1alpha1Validator: KindValidator; -// Warning: (ae-missing-release-tag) "TemplateEntityV1beta2" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export interface TemplateEntityV1beta2 extends Entity { // (undocumented) @@ -722,10 +536,6 @@ export interface TemplateEntityV1beta2 extends Entity { // (undocumented) kind: 'Template'; // (undocumented) - metadata: EntityMeta & { - title?: string; - }; - // (undocumented) spec: { type: string; parameters?: JsonObject | JsonObject[]; @@ -743,8 +553,6 @@ export interface TemplateEntityV1beta2 extends Entity { }; } -// Warning: (ae-missing-release-tag) "templateEntityV1beta2Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const templateEntityV1beta2Validator: KindValidator; @@ -764,8 +572,6 @@ export type UNSTABLE_EntityStatusItem = { // @alpha export type UNSTABLE_EntityStatusLevel = 'info' | 'warning' | 'error'; -// Warning: (ae-missing-release-tag) "UserEntityV1alpha1" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) interface UserEntityV1alpha1 extends Entity { // (undocumented) @@ -785,13 +591,9 @@ interface UserEntityV1alpha1 extends Entity { export { UserEntityV1alpha1 as UserEntity }; export { UserEntityV1alpha1 }; -// Warning: (ae-missing-release-tag) "userEntityV1alpha1Validator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const userEntityV1alpha1Validator: KindValidator; -// Warning: (ae-missing-release-tag) "Validators" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type Validators = { isValidApiVersion(value: unknown): boolean; @@ -805,18 +607,12 @@ export type Validators = { isValidTag(value: unknown): boolean; }; -// Warning: (ae-missing-release-tag) "VIEW_URL_ANNOTATION" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; // Warnings were encountered during analysis: // -// src/EntityPolicies.d.ts:13:5 - (ae-forgotten-export) The symbol "AllEntityPolicies" needs to be exported by the entry point index.d.ts -// src/EntityPolicies.d.ts:14:5 - (ae-forgotten-export) The symbol "AnyEntityPolicy" needs to be exported by the entry point index.d.ts -// src/entity/Entity.d.ts:37:5 - (ae-incompatible-release-tags) The symbol "status" is marked as @public, but its signature references "UNSTABLE_EntityStatus" which is marked as @alpha -// src/entity/policies/types.d.ts:10:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/kinds/types.d.ts:9:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/entity/Entity.d.ts:38:5 - (ae-incompatible-release-tags) The symbol "status" is marked as @public, but its signature references "UNSTABLE_EntityStatus" which is marked as @alpha // (No @packageDocumentation comment for this package) ``` diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 13c3214252..a20fb235c6 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.9.0", + "version": "0.9.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.5", - "@backstage/errors": "^0.1.1", + "@backstage/config": "^0.1.9", + "@backstage/errors": "^0.1.2", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", "ajv": "^7.0.3", @@ -40,7 +40,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.2", + "@backstage/cli": "^0.7.11", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index eca5e4b77a..0fff68d569 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -52,11 +52,12 @@ class AnyEntityPolicy implements EntityPolicy { } } +/** @public */ export const EntityPolicies = { - allOf(policies: EntityPolicy[]) { + allOf(policies: EntityPolicy[]): EntityPolicy { return new AllEntityPolicies(policies); }, - oneOf(policies: EntityPolicy[]) { + oneOf(policies: EntityPolicy[]): EntityPolicy { return new AnyEntityPolicy(policies); }, }; diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index 8843f10f5f..ca38fef183 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -21,6 +21,7 @@ import { UNSTABLE_EntityStatus } from './EntityStatus'; /** * The parts of the format that's common to all versions/kinds of entity. * + * @public * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ export type Entity = { @@ -62,6 +63,7 @@ export type Entity = { /** * Metadata fields common to all versions/kinds of entity. * + * @public * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ @@ -103,7 +105,13 @@ export type EntityMeta = JsonObject & { * The name of the entity. * * Must be unique within the catalog at any given point in time, for any - * given namespace + kind pair. + * given namespace + kind pair. This value is part of the technical + * identifier of the entity, and as such it will appear in URLs, database + * tables, entity references, and similar. It is subject to restrictions + * regarding what characters are allowed. + * + * If you want to use a different, more human readable string with fewer + * restrictions on it in user interfaces, see the `title` field below. */ name: string; @@ -112,6 +120,23 @@ export type EntityMeta = JsonObject & { */ namespace?: string; + /** + * A display name of the entity, to be presented in user interfaces instead + * of the `name` property above, when available. + * + * This field is sometimes useful when the `name` is cumbersome or ends up + * being perceived as overly technical. The title generally does not have + * as stringent format requirements on it, so it may contain special + * characters and be more explanatory. Do keep it very short though, and + * avoid situations where a title can be confused with the name of another + * entity, or where two entities share a title. + * + * Note that this is only for display purposes, and may be ignored by some + * parts of the code. Entity references still always make use of the `name` + * property, not the title. + */ + title?: string; + /** * A short (typically relatively few words, on one line) description of the * entity. @@ -143,6 +168,8 @@ export type EntityMeta = JsonObject & { /** * A relation of a specific type to another entity in the catalog. + * + * @public */ export type EntityRelation = { /** @@ -158,6 +185,8 @@ export type EntityRelation = { /** * Holds the relation data for entities. + * + * @public */ export type EntityRelationSpec = { /** @@ -178,6 +207,8 @@ export type EntityRelationSpec = { /** * A link to external information that is related to the entity. + * + * @public */ export type EntityLink = { /** diff --git a/packages/catalog-model/src/entity/EntityEnvelope.ts b/packages/catalog-model/src/entity/EntityEnvelope.ts index ed21a7d666..290738f491 100644 --- a/packages/catalog-model/src/entity/EntityEnvelope.ts +++ b/packages/catalog-model/src/entity/EntityEnvelope.ts @@ -18,6 +18,7 @@ * The envelope skeleton parts of an entity - whatever is necessary to be able * to give it a ref and pass to further validation / policy checking. * + * @public * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ */ export type EntityEnvelope = { diff --git a/packages/catalog-model/src/entity/EntityStatus.ts b/packages/catalog-model/src/entity/EntityStatus.ts index 92e1d1454f..8f79fbd615 100644 --- a/packages/catalog-model/src/entity/EntityStatus.ts +++ b/packages/catalog-model/src/entity/EntityStatus.ts @@ -18,6 +18,7 @@ import { SerializedError } from '@backstage/errors'; /** * The current status of the entity, as claimed by various sources. + * * @alpha */ export type UNSTABLE_EntityStatus = { diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index d46d839720..ff031fc1ae 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -16,11 +16,15 @@ /** * The namespace that entities without an explicit namespace fall into. + * + * @public */ export const ENTITY_DEFAULT_NAMESPACE = 'default'; /** * The keys of EntityMeta that are auto-generated. + * + * @public */ export const ENTITY_META_GENERATED_FIELDS = [ 'uid', @@ -30,6 +34,9 @@ export const ENTITY_META_GENERATED_FIELDS = [ /** * Annotations for linking to entity from catalog pages. + * + * @public */ export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; +/** @public */ export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index d05045fc78..eac55ea325 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -42,6 +42,7 @@ export { serializeEntityRef, stringifyEntityRef, } from './ref'; +export type { EntityRefContext } from './ref'; export { entityHasChanges, generateEntityEtag, diff --git a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts index 1750aece17..9511dfb05f 100644 --- a/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/DefaultNamespaceEntityPolicy.ts @@ -21,6 +21,8 @@ import { Entity } from '../Entity'; /** * Sets a default namespace if none was set. + * + * @public */ export class DefaultNamespaceEntityPolicy implements EntityPolicy { private readonly namespace: string; diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index ee95fd16ea..8655116ec6 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -29,6 +29,8 @@ import { Entity } from '../Entity'; * * This does not take into account machine generated fields such as uid, etag * and generation. + * + * @public */ export class FieldFormatEntityPolicy implements EntityPolicy { private readonly validators: Validators; diff --git a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts index 6700a935d5..1c3f865434 100644 --- a/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/NoForeignRootFieldsEntityPolicy.ts @@ -21,6 +21,8 @@ const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec']; /** * Ensures that there are no foreign root fields in the entity. + * + * @public */ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { private readonly knownFields: string[]; diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index b5a4305a3c..721dc7d2dc 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -27,6 +27,8 @@ import { EntityPolicy } from './types'; * This should be the first policy in the list, to ensure that other downstream * policies can work with a structure that is at least valid in therms of the * typescript type. + * + * @public */ export class SchemaValidEntityPolicy implements EntityPolicy { private validate: ValidateFunction | undefined; diff --git a/packages/catalog-model/src/entity/policies/types.ts b/packages/catalog-model/src/entity/policies/types.ts index 6c7f47c332..ad0485e711 100644 --- a/packages/catalog-model/src/entity/policies/types.ts +++ b/packages/catalog-model/src/entity/policies/types.ts @@ -19,12 +19,14 @@ import type { Entity } from '../Entity'; /** * A policy for validation or mutation to be applied to entities as they are * entering the system. + * + * @public */ export type EntityPolicy = { /** * Applies validation or mutation on an entity. * - * @param entity The entity, as validated/mutated so far in the policy tree + * @param entity - The entity, as validated/mutated so far in the policy tree * @returns The incoming entity, or a mutated version of the same, or * undefined if this processor could not handle the entity * @throws An error if the entity should be rejected diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index e2d9c5b203..c1b9383eeb 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -41,7 +41,8 @@ function parseRefString(ref: string): { * Extracts the kind, namespace and name that form the name triplet of the * given entity. * - * @param entity An entity + * @public + * @param entity - An entity * @returns The complete entity name */ export function getEntityName(entity: Entity): EntityName { @@ -54,8 +55,10 @@ export function getEntityName(entity: Entity): EntityName { /** * The context of defaults that entity reference parsing happens within. + * + * @public */ -type EntityRefContext = { +export type EntityRefContext = { /** The default kind, if none is given in the reference */ defaultKind?: string; /** The default namespace, if none is given in the reference */ @@ -70,8 +73,9 @@ type EntityRefContext = { * otherwise specified as part of the options, and will throw an error if no * kind was specified in the input reference and no default kind was given. * - * @param ref The reference to parse - * @param context The context of defaults that the parsing happens within + * @public + * @param ref - The reference to parse + * @param context - The context of defaults that the parsing happens within * @returns A complete entity name */ export function parseEntityName( @@ -99,8 +103,9 @@ export function parseEntityName( * The options object can contain default values for the kind and namespace, * that will be used if the input reference did not specify any. * - * @param ref The reference to parse - * @param context The context of defaults that the parsing happens within + * @public + * @param ref - The reference to parse + * @param context - The context of defaults that the parsing happens within * @returns The compound form of the reference */ export function parseEntityRef( @@ -111,6 +116,7 @@ export function parseEntityRef( namespace: string; name: string; }; +/** @public */ export function parseEntityRef( ref: EntityRef, context?: { defaultKind: string }, @@ -119,6 +125,7 @@ export function parseEntityRef( namespace?: string; name: string; }; +/** @public */ export function parseEntityRef( ref: EntityRef, context?: { defaultNamespace: string }, @@ -170,8 +177,9 @@ export function parseEntityRef( * special/reserved characters, it outputs the string form, otherwise it * outputs the compound form. * + * @public * @deprecated Use `stringifyEntityRef` instead - * @param ref The reference to serialize + * @param ref - The reference to serialize * @returns The same reference on either string or compound form */ export function serializeEntityRef( @@ -219,7 +227,8 @@ export function serializeEntityRef( * all parts of the name to lowercase and inserts the default namespace if needed. * It is typically not the best way to represent the entity reference to the user. * - * @param ref The reference to serialize + * @public + * @param ref - The reference to serialize * @returns The same reference on either string or compound form */ export function stringifyEntityRef( @@ -248,9 +257,10 @@ export function stringifyEntityRef( * The comparison is case insensitive, and all of kind, namespace, and name * must match (after applying the optional context to the ref). * - * @param entity The entity to match - * @param ref A string or compound entity ref - * @param context An optional context of default kind and namespace, that apply + * @public + * @param entity - The entity to match + * @param ref - A string or compound entity ref + * @param context - An optional context of default kind and namespace, that apply * to the ref if given * @returns True if matching, false otherwise */ diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts index 84b6845347..314f3b7737 100644 --- a/packages/catalog-model/src/entity/util.ts +++ b/packages/catalog-model/src/entity/util.ts @@ -21,6 +21,7 @@ import { Entity } from './Entity'; /** * Generates a new random UID for an entity. * + * @public * @returns A string with enough randomness to uniquely identify an entity */ export function generateEntityUid(): string { @@ -30,6 +31,7 @@ export function generateEntityUid(): string { /** * Generates a new random Etag for an entity. * + * @public * @returns A string with enough randomness to uniquely identify an entity * revision */ @@ -48,8 +50,9 @@ export function generateEntityEtag(): string { * account. It only compares the actual input entity data, i.e. metadata and * spec. * - * @param previous The old state of the entity - * @param next The new state of the entity + * @public + * @param previous - The old state of the entity + * @param next - The new state of the entity */ export function entityHasChanges(previous: Entity, next: Entity): boolean { const e1 = lodash.cloneDeep(previous); @@ -98,8 +101,9 @@ export function entityHasChanges(previous: Entity, next: Entity): boolean { * The previous revision is expected to be a complete model loaded from the * catalog, including the uid, etag and generation fields. * - * @param previous The old state of the entity - * @param next The new state of the entity + * @public + * @param previous - The old state of the entity + * @param next - The new state of the entity * @returns An entity with the merged state of both */ export function generateUpdatedEntity(previous: Entity, next: Entity): Entity { diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 2bcd9c7040..310b6695eb 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/API.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface ApiEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'API'; @@ -30,5 +31,6 @@ export interface ApiEntityV1alpha1 extends Entity { }; } +/** @public */ export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 5312d87a35..6777778409 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Component.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface ComponentEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Component'; @@ -33,5 +34,6 @@ export interface ComponentEntityV1alpha1 extends Entity { }; } +/** @public */ export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index c832f35ee0..fe777af24e 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Domain.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface DomainEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Domain'; @@ -26,5 +27,6 @@ export interface DomainEntityV1alpha1 extends Entity { }; } +/** @public */ export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 263ba311ad..f6aab856f0 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Group.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface GroupEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Group'; @@ -34,5 +35,6 @@ export interface GroupEntityV1alpha1 extends Entity { }; } +/** @public */ export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index 44f04b2c39..f2ae4bcc91 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Location.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface LocationEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Location'; @@ -28,5 +29,6 @@ export interface LocationEntityV1alpha1 extends Entity { }; } +/** @public */ export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts index 211164f4c1..acc6ab5245 100644 --- a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Resource.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface ResourceEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'Resource'; @@ -29,5 +30,6 @@ export interface ResourceEntityV1alpha1 extends Entity { }; } +/** @public */ export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 129e3fea50..5f90351480 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/System.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface SystemEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'System'; @@ -27,5 +28,6 @@ export interface SystemEntityV1alpha1 extends Entity { }; } +/** @public */ export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts index 577e5ec9e2..e708d4b387 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -15,16 +15,14 @@ */ import { JsonObject } from '@backstage/config'; -import type { Entity, EntityMeta } from '../entity/Entity'; +import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/Template.v1beta2.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface TemplateEntityV1beta2 extends Entity { apiVersion: 'backstage.io/v1beta2'; kind: 'Template'; - metadata: EntityMeta & { - title?: string; - }; spec: { type: string; parameters?: JsonObject | JsonObject[]; @@ -40,5 +38,6 @@ export interface TemplateEntityV1beta2 extends Entity { }; } +/** @public */ export const templateEntityV1beta2Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index 65ebdb3262..d719ab35ef 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -18,6 +18,7 @@ import type { Entity } from '../entity/Entity'; import schema from '../schema/kinds/User.v1alpha1.schema.json'; import { ajvCompiledJsonSchemaValidator } from './util'; +/** @public */ export interface UserEntityV1alpha1 extends Entity { apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1'; kind: 'User'; @@ -31,5 +32,6 @@ export interface UserEntityV1alpha1 extends Entity { }; } +/** @public */ export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(schema); diff --git a/packages/catalog-model/src/kinds/relations.ts b/packages/catalog-model/src/kinds/relations.ts index 57977e7168..7546d1508b 100644 --- a/packages/catalog-model/src/kinds/relations.ts +++ b/packages/catalog-model/src/kinds/relations.ts @@ -25,40 +25,60 @@ Naming rules for relations in priority order: /** * An ownership relation where the owner is usually an organizational * entity (user or group), and the other entity can be anything. + * + * @public */ export const RELATION_OWNED_BY = 'ownedBy'; +/** @public */ export const RELATION_OWNER_OF = 'ownerOf'; /** * A relation with an API entity, typically from a component + * + * @public */ export const RELATION_CONSUMES_API = 'consumesApi'; +/** @public */ export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; +/** @public */ export const RELATION_PROVIDES_API = 'providesApi'; +/** @public */ export const RELATION_API_PROVIDED_BY = 'apiProvidedBy'; /** * A relation denoting a dependency on another entity. + * + * @public */ export const RELATION_DEPENDS_ON = 'dependsOn'; +/** @public */ export const RELATION_DEPENDENCY_OF = 'dependencyOf'; /** * A parent/child relation to build up a tree, used for example to describe * the organizational structure between groups. + * + * @public */ export const RELATION_PARENT_OF = 'parentOf'; +/** @public */ export const RELATION_CHILD_OF = 'childOf'; /** * A membership relation, typically for users in a group. + * + * @public */ export const RELATION_MEMBER_OF = 'memberOf'; +/** @public */ export const RELATION_HAS_MEMBER = 'hasMember'; /** * A part/whole relation, typically for components in a system and systems * in a domain. + * + * @public */ export const RELATION_PART_OF = 'partOf'; +/** @public */ export const RELATION_HAS_PART = 'hasPart'; diff --git a/packages/catalog-model/src/kinds/types.ts b/packages/catalog-model/src/kinds/types.ts index 0ec0d313bb..f4bc151384 100644 --- a/packages/catalog-model/src/kinds/types.ts +++ b/packages/catalog-model/src/kinds/types.ts @@ -18,12 +18,14 @@ import { Entity } from '../entity'; /** * Validates entities of a certain kind. + * + * @public */ export type KindValidator = { /** * Validates the entity as a known entity kind. * - * @param entity The entity to validate + * @param entity - The entity to validate * @returns Resolves to true, if the entity was of a kind that was known and * handled by this validator, and was found to be valid. Resolves to false, * if the entity was not of a kind that was known by this validator. diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 137d36a0da..4bcabdb705 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -14,8 +14,11 @@ * limitations under the License. */ +/** @public */ export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; +/** @public */ export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; +/** @public */ export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index 91209e7a2a..cf12031dc1 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -23,8 +23,9 @@ import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION } from './annotation'; * Note that the return type is not `LocationSpec`, because we do not want to * conflate the string form with the additional properties of that type. * - * @param ref A string-form location reference, e.g. 'url:https://host' - * @returns A location reference, e.g. { type: 'url', target: 'https://host' } + * @public + * @param ref - A string-form location reference, e.g. `'url:https://host'` + * @returns A location reference, e.g. `{ type: 'url', target: 'https://host' }` */ export function parseLocationReference(ref: string): { type: string; @@ -67,8 +68,9 @@ export function parseLocationReference(ref: string): { * Note that the input type is not `LocationSpec`, because we do not want to * conflate the string form with the additional properties of that type. * - * @param ref A location reference, e.g. { type: 'url', target: 'https://host' } - * @returns A string-form location reference, e.g. 'url:https://host' + * @public + * @param ref - A location reference, e.g. `{ type: 'url', target: 'https://host' }` + * @returns A string-form location reference, e.g. `'url:https://host'` */ export function stringifyLocationReference(ref: { type: string; @@ -89,8 +91,10 @@ export function stringifyLocationReference(ref: { * Returns the source code location of the Entity, to the extent that one exists. * * If the returned location type is of type 'url', the target should be readable at least - * using the UrlReader from @backstage/backend-common. If it is not of type 'url', the caller + * using the UrlReader from `@backstage/backend-common`. If it is not of type 'url', the caller * needs to have explicit handling of each location type or signal that it is not supported. + * + * @public */ export function getEntitySourceLocation(entity: Entity): { type: string; diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts index 9837ce384d..1d16a9047d 100644 --- a/packages/catalog-model/src/location/types.ts +++ b/packages/catalog-model/src/location/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ export type LocationSpec = { type: string; target: string; @@ -23,6 +24,7 @@ export type LocationSpec = { presence?: 'optional' | 'required'; }; +/** @public */ export type Location = { id: string; } & LocationSpec; diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts index 4857fc76bc..f6680d6eee 100644 --- a/packages/catalog-model/src/location/validation.ts +++ b/packages/catalog-model/src/location/validation.ts @@ -17,7 +17,10 @@ import * as yup from 'yup'; import { LocationSpec, Location } from './types'; -/** @deprecated */ +/** + * @public + * @deprecated Use JSONSchema or validators instead. + */ export const locationSpecSchema = yup .object({ type: yup.string().required(), @@ -27,7 +30,10 @@ export const locationSpecSchema = yup .noUnknown() .required(); -/** @deprecated */ +/** + * @public + * @deprecated Use JSONSchema or validators instead. + */ export const locationSchema = yup .object({ id: yup.string().required(), @@ -37,7 +43,10 @@ export const locationSchema = yup .noUnknown() .required(); -/** @deprecated */ +/** + * @public + * @deprecated Use JSONSchema or validators instead. + */ export const analyzeLocationSchema = yup .object<{ location: LocationSpec }>({ location: locationSpecSchema, diff --git a/packages/catalog-model/src/schema/EntityMeta.schema.json b/packages/catalog-model/src/schema/EntityMeta.schema.json index ff0f9c84a8..c834bfb113 100644 --- a/packages/catalog-model/src/schema/EntityMeta.schema.json +++ b/packages/catalog-model/src/schema/EntityMeta.schema.json @@ -53,6 +53,12 @@ "examples": ["default", "admin"], "minLength": 1 }, + "title": { + "type": "string", + "description": "A display name of the entity, to be presented in user interfaces instead of the name property, when available.", + "examples": ["React SSR Template"], + "minLength": 1 + }, "description": { "type": "string", "description": "A short (typically relatively few words, on one line) description of the entity." diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index b14cdb8ee0..d68eac936d 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -69,17 +69,6 @@ "kind": { "enum": ["Template"] }, - "metadata": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The nice display name for the template.", - "examples": ["React SSR Template"], - "minLength": 1 - } - } - }, "spec": { "type": "object", "required": ["type", "steps"], diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 50de5a1cea..1fbb3be95c 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -17,10 +17,13 @@ import { JsonValue } from '@backstage/config'; import { JSONSchema7 } from 'json-schema'; +/** @public */ export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; /** * A complete entity name, with the full kind-namespace-name triplet. + * + * @public */ export type EntityName = { kind: string; @@ -36,6 +39,8 @@ export type EntityName = { * * Left-out parts of the reference need to be handled by the application, * either by rejecting the reference or by falling back to default values. + * + * @public */ export type EntityRef = | string diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 7c9736baf7..5fcd2ee755 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -19,16 +19,18 @@ import lodash from 'lodash'; /** * Contains various helper validation and normalization functions that can be * composed to form a Validator. + * + * @public */ export class CommonValidatorFunctions { /** * Checks that the value is on the form or , and validates * those parts separately. * - * @param value The value to check - * @param separator The separator between parts - * @param isValidPrefix Checks that the part before the separator is valid, if present - * @param isValidSuffix Checks that the part after the separator (or the entire value if there is no separator) is valid + * @param value - The value to check + * @param separator - The separator between parts + * @param isValidPrefix - Checks that the part before the separator is valid, if present + * @param isValidSuffix - Checks that the part after the separator (or the entire value if there is no separator) is valid */ static isValidPrefixAndOrSuffix( value: unknown, @@ -53,7 +55,7 @@ export class CommonValidatorFunctions { /** * Checks that the value can be safely transferred as JSON. * - * @param value The value to check + * @param value - The value to check */ static isJsonSafe(value: unknown): boolean { try { @@ -66,7 +68,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid DNS subdomain name. * - * @param value The value to check + * @param value - The value to check * @see https://tools.ietf.org/html/rfc1123 */ static isValidDnsSubdomain(value: unknown): boolean { @@ -81,7 +83,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid DNS label. * - * @param value The value to check + * @param value - The value to check * @see https://tools.ietf.org/html/rfc1123 */ static isValidDnsLabel(value: unknown): boolean { @@ -96,7 +98,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid URL. * - * @param value The value to check + * @param value - The value to check */ static isValidUrl(value: unknown): boolean { if (typeof value !== 'string') { @@ -115,7 +117,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a non empty string value. * - * @param value The value to check + * @param value - The value to check */ static isValidString(value: unknown): boolean { return typeof value === 'string' && value?.trim()?.length >= 1; diff --git a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts index 86d27e7b13..2423c47bef 100644 --- a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts @@ -20,6 +20,7 @@ import { CommonValidatorFunctions } from './CommonValidatorFunctions'; * Contains validation functions that match the Kubernetes spec, usable to * build a catalog that is compatible with those rule sets. * + * @public * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set diff --git a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts index aa865bcf1c..daa44ffaf1 100644 --- a/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts +++ b/packages/catalog-model/src/validation/entityEnvelopeSchemaValidator.ts @@ -22,7 +22,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; /** * Creates a validation function that takes some arbitrary data, and either * returns that data cast to an {@link EntityEnvelope} (or the given subtype) - * if it matches that schema, or throws a {@link TypeError} describing the + * if it matches that schema, or throws a {@link globals#TypeError} describing the * errors. * * Note that this validator is only meant for applying the base schema checks; @@ -38,6 +38,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * - EntityMeta * - common# * + * @public * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema */ export function entityEnvelopeSchemaValidator< diff --git a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts index a295fac205..535dba9842 100644 --- a/packages/catalog-model/src/validation/entityKindSchemaValidator.ts +++ b/packages/catalog-model/src/validation/entityKindSchemaValidator.ts @@ -22,20 +22,20 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * Creates a validation function that takes some arbitrary data, and either * returns that data cast to a `T` if it matches that schema, or `false` if the * schema apiVersion/kind didn't apply to that data, or throws a - * {@link TypeError} describing actual errors. + * {@link globals#TypeError} describing actual errors. * * This validator is highly specialized, in that it has special treatment of * the `kind` and `apiVersion` root keys. This only works if your schema has * their rule set to `"enum"`: * - *
- *   "apiVersion": {
- *      "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"]
- *   },
- *   "kind": {
- *     "enum": ["Group"]
- *   },
- * 
+ * ``` + * "apiVersion": { + * "enum": ["backstage.io/v1alpha1", "backstage.io/v1beta1"] + * }, + * "kind": { + * "enum": ["Group"] + * }, + * ``` * * In the above example, the created validator will return `false` if and only * if the kind and/or apiVersion mismatch. @@ -52,6 +52,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * - EntityMeta * - common# * + * @public * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema */ export function entityKindSchemaValidator( diff --git a/packages/catalog-model/src/validation/entitySchemaValidator.test.ts b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts index 3dfe307f92..b486de1df2 100644 --- a/packages/catalog-model/src/validation/entitySchemaValidator.test.ts +++ b/packages/catalog-model/src/validation/entitySchemaValidator.test.ts @@ -30,6 +30,8 @@ describe('entitySchemaValidator', () => { generation: 13, name: 'test', namespace: 'ns', + title: 'My Component, Yay', + description: 'Yeah this is probably the best component so far', labels: { 'backstage.io/custom': 'ValueStuff', }, @@ -192,6 +194,21 @@ describe('entitySchemaValidator', () => { expect(() => validator(entity)).toThrow(/namespace/); }); + it('accepts missing title', () => { + delete entity.metadata.title; + expect(() => validator(entity)).not.toThrow(); + }); + + it('rejects bad title type', () => { + entity.metadata.title = 7; + expect(() => validator(entity)).toThrow(/title/); + }); + + it('rejects empty title', () => { + entity.metadata.title = ''; + expect(() => validator(entity)).toThrow(/title/); + }); + it('accepts missing description', () => { delete entity.metadata.description; expect(() => validator(entity)).not.toThrow(); diff --git a/packages/catalog-model/src/validation/entitySchemaValidator.ts b/packages/catalog-model/src/validation/entitySchemaValidator.ts index 8683ab6de6..dd110efd75 100644 --- a/packages/catalog-model/src/validation/entitySchemaValidator.ts +++ b/packages/catalog-model/src/validation/entitySchemaValidator.ts @@ -22,7 +22,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; /** * Creates a validation function that takes some arbitrary data, and either * returns that data cast to an {@link Entity} (or the given subtype) if it - * matches that schema, or throws a {@link TypeError} describing the errors. + * matches that schema, or throws a {@link globals#TypeError} describing the errors. * * Note that this validator is only meant for applying the base schema checks; * it does not take custom policies or additional processor based validation @@ -37,6 +37,7 @@ import { compileAjvSchema, throwAjvError } from './ajv'; * - EntityMeta * - common# * + * @public * @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema */ export function entitySchemaValidator( diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index 0ddd9d1088..48eff566f2 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -30,6 +30,7 @@ const defaultValidators: Validators = { isValidTag: CommonValidatorFunctions.isValidDnsLabel, }; +/** @public */ export function makeValidator(overrides: Partial = {}): Validators { return { ...defaultValidators, diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index cfa9d845bc..23d639c166 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ export type Validators = { isValidApiVersion(value: unknown): boolean; isValidKind(value: unknown): boolean; diff --git a/packages/cli-common/CHANGELOG.md b/packages/cli-common/CHANGELOG.md index 52a247f57a..8a2a903efd 100644 --- a/packages/cli-common/CHANGELOG.md +++ b/packages/cli-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/cli-common +## 0.1.3 + +### Patch Changes + +- d1da88a19: Properly export all used types. + ## 0.1.2 ### Patch Changes diff --git a/packages/cli-common/api-report.md b/packages/cli-common/api-report.md index d6d1c4fc36..f129fa3045 100644 --- a/packages/cli-common/api-report.md +++ b/packages/cli-common/api-report.md @@ -3,19 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -// Warning: (ae-missing-release-tag) "findPaths" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function findPaths(searchDir: string): Paths; -// Warning: (ae-missing-release-tag) "isChildPath" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function isChildPath(base: string, path: string): boolean; -// Warning: (ae-missing-release-tag) "Paths" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type Paths = { ownDir: string; ownRoot: string; @@ -27,9 +21,8 @@ export type Paths = { resolveTargetRoot: ResolveFunc; }; -// Warnings were encountered during analysis: -// -// src/paths.d.ts:7:5 - (ae-forgotten-export) The symbol "ResolveFunc" needs to be exported by the entry point index.d.ts +// @public (undocumented) +export type ResolveFunc = (...paths: string[]) => string; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 16f10717f9..c643192df3 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.2", + "version": "0.1.3", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli-common/src/index.ts b/packages/cli-common/src/index.ts index 1f4f17fe9f..ed8158366d 100644 --- a/packages/cli-common/src/index.ts +++ b/packages/cli-common/src/index.ts @@ -16,4 +16,4 @@ export { findPaths } from './paths'; export { isChildPath } from './isChildPath'; -export type { Paths } from './paths'; +export type { Paths, ResolveFunc } from './paths'; diff --git a/packages/cli-common/src/isChildPath.ts b/packages/cli-common/src/isChildPath.ts index f48c92db32..f66a0cfa94 100644 --- a/packages/cli-common/src/isChildPath.ts +++ b/packages/cli-common/src/isChildPath.ts @@ -18,6 +18,8 @@ import { relative, isAbsolute } from 'path'; /** * Checks if path is the same as or a child path of base. + * + * @public */ export function isChildPath(base: string, path: string): boolean { const relativePath = relative(base, path); diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index 12a8484797..90239dfa4a 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -17,10 +17,15 @@ import fs from 'fs'; import { dirname, resolve as resolvePath } from 'path'; +/** @public */ export type ResolveFunc = (...paths: string[]) => string; -// Common paths and resolve functions used by the cli. -// Currently assumes it is being executed within a monorepo. +/** + * Common paths and resolve functions used by the cli. + * Currently assumes it is being executed within a monorepo. + * + * @public + */ export type Paths = { // Root dir of the cli itself, containing package.json ownDir: string; @@ -100,6 +105,7 @@ export function findOwnRootDir(ownDir: string) { /** * Find paths related to a package and its execution context. * + * @public * @example * * const paths = findPaths(__dirname) diff --git a/packages/cli/.snyk b/packages/cli/.snyk new file mode 100644 index 0000000000..8fbbfa574c --- /dev/null +++ b/packages/cli/.snyk @@ -0,0 +1,10 @@ +# 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-ANSIHTML-1296849: + - 'webpack-dev-server > ansi-html': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-03-06T17:18:55.019Z + created: 2021-09-06T17:18:55.027Z +patch: {} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 064438d17e..f404b49e48 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/cli +## 0.7.11 + +### Patch Changes + +- 13895db37: Support importing font files in tests. + This fixes remaining issues from [#7019](https://github.com/backstage/backstage/pull/7019). +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config-loader@0.6.8 + - @backstage/config@0.1.9 + +## 0.7.10 + +### Patch Changes + +- 5e803edb8: Added support for importing font files. Imports in CSS via `url()` are supported for the final frontend bundle, but not for packages that are built for publishing. Module imports of fonts files from TypeScript are supported everywhere. +- b5118ff76: Updated dependencies + ## 0.7.9 ### Patch Changes diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts index 879e9b0b05..0bd0922004 100644 --- a/packages/cli/asset-types/asset-types.d.ts +++ b/packages/cli/asset-types/asset-types.d.ts @@ -68,6 +68,26 @@ declare module '*.svg' { export default src; } +declare module '*.eot' { + const src: string; + export default src; +} + +declare module '*.woff' { + const src: string; + export default src; +} + +declare module '*.woff2' { + const src: string; + export default src; +} + +declare module '*.ttf' { + const src: string; + export default src; +} + declare module '*.css' { const classes: { readonly [key: string]: string }; export default classes; diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index e2ff338b4e..4e11a63a6b 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -80,9 +80,8 @@ async function getConfig() { transform: { '\\.esm\\.js$': require.resolve('./jestEsmTransform.js'), // See jestEsmTransform.js '\\.(js|jsx|ts|tsx)$': require.resolve('@sucrase/jest-plugin'), - '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve( - './jestFileTransform.js', - ), + '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$': + require.resolve('./jestFileTransform.js'), '\\.(yaml)$': require.resolve('yaml-jest'), }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 13d146de72..12cc584e2d 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.7.9", + "version": "0.7.11", "private": false, "publishConfig": { "access": "public" @@ -30,9 +30,9 @@ "dependencies": { "@babel/core": "^7.4.4", "@babel/plugin-transform-modules-commonjs": "^7.4.4", - "@backstage/cli-common": "^0.1.2", - "@backstage/config": "^0.1.8", - "@backstage/config-loader": "^0.6.7", + "@backstage/cli-common": "^0.1.3", + "@backstage/config": "^0.1.9", + "@backstage/config-loader": "^0.6.8", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", "@lerna/project": "^4.0.0", @@ -51,7 +51,7 @@ "@svgr/rollup": "5.5.x", "@svgr/webpack": "5.5.x", "@types/webpack-env": "^1.15.2", - "@typescript-eslint/eslint-plugin": "^v4.28.3", + "@typescript-eslint/eslint-plugin": "^v4.30.0", "@typescript-eslint/parser": "^v4.28.3", "@yarnpkg/lockfile": "^1.1.0", "babel-plugin-dynamic-import-node": "^2.3.3", @@ -120,12 +120,12 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/config": "^0.1.8", - "@backstage/core-components": "^0.3.3", - "@backstage/core-plugin-api": "^0.1.6", - "@backstage/core-app-api": "^0.1.10", - "@backstage/dev-utils": "^0.2.7", + "@backstage/backend-common": "^0.9.2", + "@backstage/config": "^0.1.9", + "@backstage/core-components": "^0.4.1", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/core-app-api": "^0.1.12", + "@backstage/dev-utils": "^0.2.9", "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.10", "@types/diff": "^5.0.0", diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 60a5f0efca..3c95a87bdd 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -80,7 +80,17 @@ export const makeConfigs = async ( postcss(), forwardFileImports({ exclude: /\.icon\.svg$/, - include: [/\.svg$/, /\.png$/, /\.gif$/, /\.jpg$/, /\.jpeg$/], + include: [ + /\.svg$/, + /\.png$/, + /\.gif$/, + /\.jpg$/, + /\.jpeg$/, + /\.eot$/, + /\.woff$/, + /\.woff2$/, + /\.ttf$/, + ], }), json(), yaml(), diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 34a441139c..b0a9b25466 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -89,6 +89,17 @@ export const transforms = (options: TransformOptions): Transforms => { name: 'static/[name].[hash:8].[ext]', }, }, + { + test: /\.(eot|woff|woff2|ttf)$/, + use: [ + { + loader: require.resolve('file-loader'), + options: { + name: 'static/[name].[hash:8].[ext]', + }, + }, + ], + }, { test: /\.ya?ml$/, use: require.resolve('yml-loader'), diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 74a3022bf5..aa21ebc3a1 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/codemods +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.1 + - @backstage/cli-common@0.1.3 + - @backstage/core-app-api@0.1.12 + - @backstage/core-plugin-api@0.1.7 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.0 + - @backstage/core-app-api@0.1.11 + ## 0.1.11 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 04bf0ac014..0fbafe7fe2 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.11", + "version": "0.1.13", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "backstage-codemods": "bin/backstage-codemods" }, "dependencies": { - "@backstage/cli-common": "0.1.2", + "@backstage/cli-common": "0.1.3", "@backstage/core-app-api": "*", "@backstage/core-components": "*", "@backstage/core-plugin-api": "*", diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index aee4cf453f..88479bbb18 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/config-loader +## 0.6.8 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config@0.1.9 + ## 0.6.7 ### Patch Changes diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 38aa0b1ebb..a674f624b0 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -7,30 +7,31 @@ import { AppConfig } from '@backstage/config'; import { JsonObject } from '@backstage/config'; import { JSONSchema7 } from 'json-schema'; -// Warning: (ae-missing-release-tag) "ConfigSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type ConfigSchema = { process( appConfigs: AppConfig[], - options?: ConfigProcessingOptions, + options?: ConfigSchemaProcessingOptions, ): AppConfig[]; serialize(): JsonObject; }; -// Warning: (ae-forgotten-export) The symbol "CONFIG_VISIBILITIES" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ConfigVisibility" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export type ConfigVisibility = typeof CONFIG_VISIBILITIES[number]; +export type ConfigSchemaProcessingOptions = { + visibility?: ConfigVisibility[]; + valueTransform?: TransformFunc; + withFilteredKeys?: boolean; +}; + +// @public +export type ConfigVisibility = 'frontend' | 'backend' | 'secret'; -// Warning: (ae-missing-release-tag) "loadConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) +export type EnvFunc = (name: string) => Promise; + +// @public export function loadConfig(options: LoadConfigOptions): Promise; -// Warning: (ae-missing-release-tag) "LoadConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type LoadConfigOptions = { configRoot: string; @@ -43,28 +44,35 @@ export type LoadConfigOptions = { }; }; -// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "loadConfigSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export function loadConfigSchema(options: Options): Promise; +export function loadConfigSchema( + options: LoadConfigSchemaOptions, +): Promise; + +// @public (undocumented) +export type LoadConfigSchemaOptions = + | { + dependencies: string[]; + } + | { + serialized: JsonObject; + }; -// Warning: (ae-missing-release-tag) "mergeConfigSchemas" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7; -// Warning: (ae-missing-release-tag) "readEnvConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function readEnvConfig(env: { [name: string]: string | undefined; }): AppConfig[]; -// Warnings were encountered during analysis: -// -// src/lib/schema/types.d.ts:83:5 - (ae-forgotten-export) The symbol "ConfigProcessingOptions" needs to be exported by the entry point index.d.ts -// src/loader.d.ts:13:5 - (ae-forgotten-export) The symbol "EnvFunc" needs to be exported by the entry point index.d.ts +// @public +export type TransformFunc = ( + value: T, + context: { + visibility: ConfigVisibility; + }, +) => T | undefined; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index f810841d3e..0f5bac2ffc 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.6.7", + "version": "0.6.8", "private": false, "publishConfig": { "access": "public", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.1", - "@backstage/config": "^0.1.7", + "@backstage/cli-common": "^0.1.3", + "@backstage/config": "^0.1.9", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "chokidar": "^3.5.2", diff --git a/packages/config-loader/src/index.ts b/packages/config-loader/src/index.ts index f605d53115..29976b52de 100644 --- a/packages/config-loader/src/index.ts +++ b/packages/config-loader/src/index.ts @@ -15,6 +15,13 @@ */ export { readEnvConfig, loadConfigSchema, mergeConfigSchemas } from './lib'; -export type { ConfigSchema, ConfigVisibility } from './lib'; +export type { + ConfigSchema, + ConfigSchemaProcessingOptions, + ConfigVisibility, + EnvFunc, + LoadConfigSchemaOptions, + TransformFunc, +} from './lib'; export { loadConfig } from './loader'; export type { LoadConfigOptions } from './loader'; diff --git a/packages/config-loader/src/lib/env.ts b/packages/config-loader/src/lib/env.ts index 7d28e6b6ef..b244c06c64 100644 --- a/packages/config-loader/src/lib/env.ts +++ b/packages/config-loader/src/lib/env.ts @@ -38,6 +38,8 @@ const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; * For example, to set the config app.title to "My Title", use the following: * * APP_CONFIG_app_title='"My Title"' + * + * @public */ export function readEnvConfig(env: { [name: string]: string | undefined; diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index 4236fd2d17..a36d2eb9a2 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -108,6 +108,8 @@ export function compileConfigSchemas( /** * Given a list of configuration schemas from packages, merge them * into a single json schema. + * + * @public */ export function mergeConfigSchemas(schemas: JSONSchema[]): JSONSchema { const merged = mergeAllOf( diff --git a/packages/config-loader/src/lib/schema/index.ts b/packages/config-loader/src/lib/schema/index.ts index 851be36f84..1dcb9d7b4b 100644 --- a/packages/config-loader/src/lib/schema/index.ts +++ b/packages/config-loader/src/lib/schema/index.ts @@ -16,4 +16,10 @@ export { mergeConfigSchemas } from './compile'; export { loadConfigSchema } from './load'; -export type { ConfigSchema, ConfigVisibility } from './types'; +export type { LoadConfigSchemaOptions } from './load'; +export type { + ConfigSchema, + ConfigVisibility, + ConfigSchemaProcessingOptions, + TransformFunc, +} from './types'; diff --git a/packages/config-loader/src/lib/schema/load.ts b/packages/config-loader/src/lib/schema/load.ts index b67bd0002f..4620961310 100644 --- a/packages/config-loader/src/lib/schema/load.ts +++ b/packages/config-loader/src/lib/schema/load.ts @@ -24,7 +24,8 @@ import { CONFIG_VISIBILITIES, } from './types'; -type Options = +/** @public */ +export type LoadConfigSchemaOptions = | { dependencies: string[]; } @@ -34,9 +35,11 @@ type Options = /** * Loads config schema for a Backstage instance. + * + * @public */ export async function loadConfigSchema( - options: Options, + options: LoadConfigSchemaOptions, ): Promise { let schemas: ConfigSchemaPackageEntry[]; diff --git a/packages/config-loader/src/lib/schema/types.ts b/packages/config-loader/src/lib/schema/types.ts index 17090ad53c..ef6a197f55 100644 --- a/packages/config-loader/src/lib/schema/types.ts +++ b/packages/config-loader/src/lib/schema/types.ts @@ -37,8 +37,10 @@ export const CONFIG_VISIBILITIES = ['frontend', 'backend', 'secret'] as const; /** * A type representing the possible configuration value visibilities + * + * @public */ -export type ConfigVisibility = typeof CONFIG_VISIBILITIES[number]; +export type ConfigVisibility = 'frontend' | 'backend' | 'secret'; /** * The default configuration visibility if no other values is given. @@ -73,6 +75,8 @@ export type ValidationFunc = (configs: AppConfig[]) => ValidationResult; /** * A function used to transform primitive configuration values. + * + * @public */ export type TransformFunc = ( value: T, @@ -81,8 +85,10 @@ export type TransformFunc = ( /** * Options used to process configuration data with a schema. + * + * @public */ -type ConfigProcessingOptions = { +export type ConfigSchemaProcessingOptions = { /** * The visibilities that should be included in the output data. * If omitted, the data will not be filtered by visibility. @@ -107,11 +113,13 @@ type ConfigProcessingOptions = { /** * A loaded configuration schema that is ready to process configuration data. + * + * @public */ export type ConfigSchema = { process( appConfigs: AppConfig[], - options?: ConfigProcessingOptions, + options?: ConfigSchemaProcessingOptions, ): AppConfig[]; serialize(): JsonObject; diff --git a/packages/config-loader/src/lib/transform/index.ts b/packages/config-loader/src/lib/transform/index.ts index 5053cd7443..eaa85cca3c 100644 --- a/packages/config-loader/src/lib/transform/index.ts +++ b/packages/config-loader/src/lib/transform/index.ts @@ -17,3 +17,4 @@ export { applyConfigTransforms } from './apply'; export { createIncludeTransform } from './include'; export { createSubstitutionTransform } from './substitution'; +export type { EnvFunc } from './types'; diff --git a/packages/config-loader/src/lib/transform/types.ts b/packages/config-loader/src/lib/transform/types.ts index 20e5f88718..afd3550984 100644 --- a/packages/config-loader/src/lib/transform/types.ts +++ b/packages/config-loader/src/lib/transform/types.ts @@ -16,6 +16,7 @@ import { JsonValue } from '@backstage/config'; +/** @public */ export type EnvFunc = (name: string) => Promise; export type ReadFileFunc = (path: string) => Promise; diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 4dbfc007a7..315b44c4ee 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -27,6 +27,7 @@ import { } from './lib'; import { EnvFunc } from './lib/transform/types'; +/** @public */ export type LoadConfigOptions = { // The root directory of the config loading context. Used to find default configs. configRoot: string; @@ -60,6 +61,11 @@ export type LoadConfigOptions = { }; }; +/** + * Load configuration data. + * + * @public + */ export async function loadConfig( options: LoadConfigOptions, ): Promise { diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 742ae86a57..7691ed2865 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config +## 0.1.9 + +### Patch Changes + +- f88b2c7db: Documented `Config` interface and mark types as public. + ## 0.1.8 ### Patch Changes diff --git a/packages/config/api-report.md b/packages/config/api-report.md index 91f37894c0..068498b17a 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -3,18 +3,14 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -// Warning: (ae-missing-release-tag) "AppConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AppConfig = { context: string; data: JsonObject; filteredKeys?: string[]; }; -// Warning: (ae-missing-release-tag) "Config" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type Config = { subscribe?(onChange: () => void): { unsubscribe: () => void; @@ -37,9 +33,7 @@ export type Config = { getOptionalStringArray(key: string): string[] | undefined; }; -// Warning: (ae-missing-release-tag) "ConfigReader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class ConfigReader implements Config { constructor( data: JsonObject | undefined, @@ -83,26 +77,18 @@ export class ConfigReader implements Config { keys(): string[]; } -// Warning: (ae-missing-release-tag) "JsonArray" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface JsonArray extends Array {} -// Warning: (ae-missing-release-tag) "JsonObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type JsonObject = { [key in string]?: JsonValue; }; -// Warning: (ae-missing-release-tag) "JsonPrimitive" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type JsonPrimitive = number | string | boolean | null; -// Warning: (ae-missing-release-tag) "JsonValue" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type JsonValue = JsonObject | JsonArray | JsonPrimitive; // (No @packageDocumentation comment for this package) diff --git a/packages/config/package.json b/packages/config/package.json index 6adee62ca3..969bc44045 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.8", + "version": "0.1.9", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index c6f69e5b29..92ca0eede3 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -54,6 +54,12 @@ const errors = { }, }; +/** + * An implementation of the `Config` interface that uses a plain JavaScript object + * for the backing data, with the ability of linking multiple readers together. + * + * @public + */ export class ConfigReader implements Config { /** * A set of key paths that where removed from the config due to not being visible. diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 84de5dae70..d5f551cd69 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -14,17 +14,61 @@ * limitations under the License. */ +/** + * A type representing all allowed JSON primitive values. + * + * @public + */ export type JsonPrimitive = number | string | boolean | null; + +/** + * A type representing all allowed JSON object values. + * + * @public + */ export type JsonObject = { [key in string]?: JsonValue }; + +/** + * A type representing all allowed JSON array values. + * + * @public + */ export interface JsonArray extends Array {} + +/** + * A type representing all allowed JSON values. + * + * @public + */ export type JsonValue = JsonObject | JsonArray | JsonPrimitive; +/** + * A serialized form of configuration data that carries additional context. + * + * @public + */ export type AppConfig = { + /** + * A string representing the source of this configuration data, for example a filepath. + */ context: string; + /** + * The configuration data itself. + */ data: JsonObject; + /** + * A list of keys that where filtered out from the configuration when it was loaded. + * + * This can be used to warn the user if they try to read any of these keys. + */ filteredKeys?: string[]; }; +/** + * The interface used to represent static configuration at runtime. + * + * @public + */ export type Config = { /** * Subscribes to the configuration object in order to receive a notification @@ -37,28 +81,90 @@ export type Config = { unsubscribe: () => void; }; + /** + * Checks whether the given key is present. + */ has(key: string): boolean; + /** + * Lists all available configuration keys. + */ keys(): string[]; + /** + * Same as `getOptional`, but will throw an error if there's no value for the given key. + */ get(key?: string): T; + + /** + * Read out all configuration data for the given key. + * + * Usage of this method should be avoided as the typed alternatives provide + * much better error reporting. The main use-case of this method is to determine + * the type of a configuration value in the case where there are multiple possible + * shapes of the configuration. + */ getOptional(key?: string): T | undefined; + /** + * Same as `getOptionalConfig`, but will throw an error if there's no value for the given key. + */ getConfig(key: string): Config; + + /** + * Creates a sub-view of the configuration object. + * The configuration value at the position of the provided key must be an object. + */ getOptionalConfig(key: string): Config | undefined; + /** + * Same as `getOptionalConfigArray`, but will throw an error if there's no value for the given key. + */ getConfigArray(key: string): Config[]; + + /** + * Creates a sub-view of an array of configuration objects. + * The configuration value at the position of the provided key must be an array of objects. + */ getOptionalConfigArray(key: string): Config[] | undefined; + /** + * Same as `getOptionalNumber`, but will throw an error if there's no value for the given key. + */ getNumber(key: string): number; + + /** + * Reads a configuration value at the given key, expecting it to be a number. + */ getOptionalNumber(key: string): number | undefined; + /** + * Same as `getOptionalBoolean`, but will throw an error if there's no value for the given key. + */ getBoolean(key: string): boolean; + + /** + * Reads a configuration value at the given key, expecting it to be a boolean. + */ getOptionalBoolean(key: string): boolean | undefined; + /** + * Same as `getOptionalString`, but will throw an error if there's no value for the given key. + */ getString(key: string): string; + + /** + * Reads a configuration value at the given key, expecting it to be a string. + */ getOptionalString(key: string): string | undefined; + /** + * Same as `getOptionalStringArray`, but will throw an error if there's no value for the given key. + */ getStringArray(key: string): string[]; + + /** + * Reads a configuration value at the given key, expecting it to be an array of strings. + */ getOptionalStringArray(key: string): string[] | undefined; }; diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index db5158a697..9020661647 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/core-app-api +## 0.1.12 + +### Patch Changes + +- 841666a19: Removed deprecated internal functions. +- Updated dependencies + - @backstage/core-components@0.4.1 + - @backstage/config@0.1.9 + - @backstage/core-plugin-api@0.1.7 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.4.0 + ## 0.1.10 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 90c962be3e..5223d2b27e 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.10", + "version": "0.1.12", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.3.3", - "@backstage/config": "^0.1.8", - "@backstage/core-plugin-api": "^0.1.6", + "@backstage/core-components": "^0.4.1", + "@backstage/config": "^0.1.9", + "@backstage/core-plugin-api": "^0.1.7", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,7 +44,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.11", "@backstage/test-utils": "^0.1.17", "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core-app-api/src/extensions/componentData.test.tsx b/packages/core-app-api/src/extensions/componentData.test.tsx deleted file mode 100644 index 808ab08cf1..0000000000 --- a/packages/core-app-api/src/extensions/componentData.test.tsx +++ /dev/null @@ -1,118 +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 React from 'react'; -import { attachComponentData, getComponentData } from './componentData'; - -describe('elementData', () => { - it('should attach a single piece of data', () => { - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - - it('should attach several distinct pieces of data', () => { - const data1 = { foo: 'bar' }; - const data2 = { test: 'value' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data1); - attachComponentData(Component, 'second', data2); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data1); - expect(getComponentData(element, 'second')).toBe(data2); - }); - - it('returns undefined for missing data', () => { - const data = { foo: 'bar' }; - const Component1 = () => null; - const Component2 = () => null; - attachComponentData(Component2, 'my-data', data); - - const element1 = ; - const element2 = ; - expect(getComponentData(element1, 'missing')).toBeUndefined(); - expect(getComponentData(element2, 'missing')).toBeUndefined(); - }); - - it('should throw when attempting to overwrite data', () => { - const data = { foo: 'bar' }; - const MyComponent = () => null; - attachComponentData(MyComponent, 'my-data', data); - expect(() => attachComponentData(MyComponent, 'my-data', data)).toThrow( - 'Attempted to attach duplicate data "my-data" to component "MyComponent"', - ); - }); - - describe('works across versions', () => { - function getDataSymbol() { - const Component = () => null; - attachComponentData(Component, 'my-data', {}); - const [symbol] = Object.getOwnPropertySymbols(Component); - return symbol; - } - - it('should should be able to get data from older versions', () => { - const symbol = getDataSymbol(); - - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - expect((element as any).type[symbol].map.get('my-data')).toBe(data); - }); - - it('should should be able to attach data for older versions', () => { - const symbol = getDataSymbol(); - - const data = { foo: 'bar' }; - const Component = () => null; - (Component as any)[symbol] = { - map: new Map([['my-data', data]]), - }; - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - - it('should be able to get data from newer versions', () => { - const data = { foo: 'bar' }; - const Component = () => null; - attachComponentData(Component, 'my-data', data); - - const element = ; - const container = (global as any)[ - '__@backstage/component-data-store__' - ].get(element.type); - expect(container.map.get('my-data')).toBe(data); - }); - - it('should should be able to attach data for newer versions', () => { - const data = { foo: 'bar' }; - const Component = () => null; - (global as any)['__@backstage/component-data-store__'].set(Component, { - map: new Map([['my-data', data]]), - }); - - const element = ; - expect(getComponentData(element, 'my-data')).toBe(data); - }); - }); -}); diff --git a/packages/core-app-api/src/extensions/componentData.tsx b/packages/core-app-api/src/extensions/componentData.tsx deleted file mode 100644 index d4975d9eef..0000000000 --- a/packages/core-app-api/src/extensions/componentData.tsx +++ /dev/null @@ -1,84 +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 { ComponentType, ReactNode } from 'react'; -import { getOrCreateGlobalSingleton } from '../lib/globalObject'; - -// TODO(Rugvip): Access via symbol is deprecated, remove once on 0.3.x -const DATA_KEY = Symbol('backstage-component-data'); - -type ComponentWithData

= ComponentType

& { - [DATA_KEY]?: DataContainer; -}; - -type DataContainer = { - map: Map; -}; - -type MaybeComponentNode = ReactNode & { - type?: ComponentType & { [DATA_KEY]?: DataContainer }; -}; - -// The store is bridged across versions using the global object -const store = getOrCreateGlobalSingleton( - 'component-data-store', - () => new WeakMap, DataContainer>(), -); - -export function attachComponentData

( - component: ComponentType

, - type: string, - data: unknown, -) { - const dataComponent = component as ComponentWithData

; - - let container = store.get(component) || dataComponent[DATA_KEY]; - if (!container) { - container = { map: new Map() }; - store.set(component, container); - dataComponent[DATA_KEY] = container; - } - - if (container.map.has(type)) { - const name = component.displayName || component.name; - throw new Error( - `Attempted to attach duplicate data "${type}" to component "${name}"`, - ); - } - - container.map.set(type, data); -} - -export function getComponentData( - node: ReactNode, - type: string, -): T | undefined { - if (!node) { - return undefined; - } - - const component = (node as MaybeComponentNode).type; - if (!component) { - return undefined; - } - - const container = store.get(component) || component[DATA_KEY]; - if (!container) { - return undefined; - } - - return container.map.get(type) as T | undefined; -} diff --git a/packages/core-app-api/src/extensions/extensions.tsx b/packages/core-app-api/src/extensions/extensions.tsx deleted file mode 100644 index 199430c619..0000000000 --- a/packages/core-app-api/src/extensions/extensions.tsx +++ /dev/null @@ -1,54 +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 { Extension, RouteRef } from '@backstage/core-plugin-api'; - -type ComponentLoader = - | { - lazy: () => Promise; - } - | { - sync: T; - }; - -const ERROR_MESSAGE = 'Import this from @backstage/core-plugin-api'; - -/** @deprecated Import from @backstage/core-plugin-api instead */ -export function createRoutableExtension< - T extends (props: any) => JSX.Element | null, ->(_options: { - component: () => Promise; - mountPoint: RouteRef; -}): Extension { - throw new Error(ERROR_MESSAGE); -} - -/** @deprecated Import from @backstage/core-plugin-api instead */ -export function createComponentExtension< - T extends (props: any) => JSX.Element | null, ->(_options: { component: ComponentLoader }): Extension { - throw new Error(ERROR_MESSAGE); -} - -/** @deprecated Import from @backstage/core-plugin-api instead */ -export function createReactExtension< - T extends (props: any) => JSX.Element | null, ->(_options: { - component: ComponentLoader; - data?: Record; -}): Extension { - throw new Error(ERROR_MESSAGE); -} diff --git a/packages/core-app-api/src/plugins/collectors.ts b/packages/core-app-api/src/plugins/collectors.ts index 11e5cd51b3..0f8b65ad62 100644 --- a/packages/core-app-api/src/plugins/collectors.ts +++ b/packages/core-app-api/src/plugins/collectors.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { getComponentData } from '../extensions'; +import { BackstagePlugin, getComponentData } from '@backstage/core-plugin-api'; import { createCollector } from '../extensions/traversal'; export const pluginCollector = createCollector( diff --git a/packages/core-app-api/src/routing/collectors.test.tsx b/packages/core-app-api/src/routing/collectors.test.tsx index d37b5e3016..36d99d2938 100644 --- a/packages/core-app-api/src/routing/collectors.test.tsx +++ b/packages/core-app-api/src/routing/collectors.test.tsx @@ -31,8 +31,8 @@ import { createRouteRef, createPlugin, RouteRef, + attachComponentData, } from '@backstage/core-plugin-api'; -import { attachComponentData } from '../extensions'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( diff --git a/packages/core-app-api/src/routing/collectors.tsx b/packages/core-app-api/src/routing/collectors.tsx index fcbe3ca923..0ba83e90f8 100644 --- a/packages/core-app-api/src/routing/collectors.tsx +++ b/packages/core-app-api/src/routing/collectors.tsx @@ -15,9 +15,8 @@ */ import { isValidElement, ReactElement, ReactNode } from 'react'; -import { RouteRef } from '@backstage/core-plugin-api'; +import { RouteRef, getComponentData } from '@backstage/core-plugin-api'; import { BackstageRouteObject } from './types'; -import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged'; diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 1b56b033b5..1c195cdd2c 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/core-components +## 0.4.1 + +### Patch Changes + +- 06e275705: Fix warning produced by BottomLink component + + During development, we noticed warnings such as: + + ``` + react_devtools_backend.js:2842 Warning: validateDOMNesting(...):

cannot appear as a descendant of

. + ``` + + The BottomLink component renders a Box component within a Typography component which leads to a div tag within a p tag. + This change inverts that ordering without changing the visual appearance. + +- Updated dependencies + - @backstage/errors@0.1.2 + - @backstage/config@0.1.9 + - @backstage/core-plugin-api@0.1.7 + +## 0.4.0 + +### Minor Changes + +- 3ed78fca3: Changed the `titleComponent` prop on `ContentHeader` to accept `ReactNode` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element: + + ```diff + - + +}> + ``` + +### Patch Changes + +- e0a6aea82: Bumped `react-hook-form` to `^7.12.2` + ## 0.3.3 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index b4402f67ac..f73eee1f13 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -15,7 +15,6 @@ import { Column } from '@material-table/core'; import { CommonProps } from '@material-ui/core/OverridableComponent'; import { ComponentClass } from 'react'; import { ComponentProps } from 'react'; -import { ComponentType } from 'react'; import { Context } from 'react'; import { default as CSS_2 } from 'csstype'; import { CSSProperties } from 'react'; diff --git a/packages/core-components/package.json b/packages/core-components/package.json index e4f99fc9ec..7ac256ad30 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.3.3", + "version": "0.4.1", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.8", - "@backstage/core-plugin-api": "^0.1.6", - "@backstage/errors": "^0.1.1", + "@backstage/config": "^0.1.9", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/errors": "^0.1.2", "@backstage/theme": "^0.2.10", "@material-table/core": "^3.1.0", "@material-ui/core": "^4.12.2", @@ -58,7 +58,7 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-helmet": "6.1.0", - "react-hook-form": "^6.15.4", + "react-hook-form": "^7.12.2", "react-markdown": "^5.0.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", @@ -70,8 +70,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.10", - "@backstage/cli": "^0.7.9", + "@backstage/core-app-api": "^0.1.12", + "@backstage/cli": "^0.7.11", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core-components/src/components/Button/Button.stories.tsx b/packages/core-components/src/components/Button/Button.stories.tsx index 28deb1ad33..3451a77765 100644 --- a/packages/core-components/src/components/Button/Button.stories.tsx +++ b/packages/core-components/src/components/Button/Button.stories.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentType } from 'react'; import { Button } from './Button'; -import { MemoryRouter, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api'; import { Divider, @@ -26,11 +26,7 @@ import { Typography, Button as MaterialButton, } from '@material-ui/core'; -// We don't want to export RoutingProvider from core-app-api, but it's way easier to -// use here. This hack only works in storybook stories. -// TODO: Export a nicer to user routing provider, perhaps from test-utils -// eslint-disable-next-line monorepo/no-internal-import -import { RoutingProvider } from '@backstage/core-app-api/src/routing/RoutingProvider'; +import { wrapInTestApp } from '@backstage/test-utils'; const routeRef = createRouteRef({ id: 'storybook.test-route', @@ -45,36 +41,29 @@ export default { title: 'Inputs/Button', component: Button, decorators: [ - (Story: ComponentType<{}>) => ( - <> - - A collection of buttons that should be used in the Backstage - interface. These leverage the properties inherited from{' '} - - Material-UI Button - - , but include an opinionated set that align to the Backstage design. - + (Story: ComponentType<{}>) => + wrapInTestApp( + <> + + A collection of buttons that should be used in the Backstage + interface. These leverage the properties inherited from{' '} + + Material-UI Button + + , but include an opinionated set that align to the Backstage design. + - + - - +

-
- -
- +
- - - - ), + +
+ , + { mountedRoutes: { '/hello': routeRef } }, + ), ], }; diff --git a/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx b/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx index 69269d687d..8feca43e9a 100644 --- a/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx +++ b/packages/core-components/src/components/DependencyGraph/DefaultNode.tsx @@ -21,11 +21,11 @@ import { RenderNodeProps } from './types'; const useStyles = makeStyles((theme: BackstageTheme) => ({ node: { - fill: theme.palette.background.paper, - stroke: theme.palette.border, + fill: theme.palette.primary.light, + stroke: theme.palette.primary.light, }, text: { - fill: theme.palette.textContrast, + fill: theme.palette.primary.contrastText, }, })); diff --git a/packages/core-components/src/components/Link/Link.stories.tsx b/packages/core-components/src/components/Link/Link.stories.tsx index 516a5adcff..d9944c1c51 100644 --- a/packages/core-components/src/components/Link/Link.stories.tsx +++ b/packages/core-components/src/components/Link/Link.stories.tsx @@ -15,17 +15,9 @@ */ import React, { ComponentType } from 'react'; import { Link } from './Link'; -import { - MemoryRouter, - Route, - useLocation, - NavLink as RouterNavLink, -} from 'react-router-dom'; +import { Route, useLocation, NavLink as RouterNavLink } from 'react-router-dom'; import { createRouteRef, useRouteRef } from '@backstage/core-plugin-api'; -// We don't want to export RoutingProvider from core-app-api, but it's way easier to -// use here. This hack only works in storybook stories. -// eslint-disable-next-line monorepo/no-internal-import -import { RoutingProvider } from '@backstage/core-app-api/src/routing/RoutingProvider'; +import { wrapInTestApp } from '@backstage/test-utils'; const routeRef = createRouteRef({ id: 'storybook.test-route', @@ -40,23 +32,16 @@ export default { title: 'Navigation/Link', component: Link, decorators: [ - (Story: ComponentType<{}>) => ( - - + (Story: ComponentType<{}>) => + wrapInTestApp( +
-
- -
- +
- - - ), + +
, + { mountedRoutes: { '/hello': routeRef } }, + ), ], }; diff --git a/packages/core-components/src/layout/BottomLink/BottomLink.tsx b/packages/core-components/src/layout/BottomLink/BottomLink.tsx index fdc64a9b90..74912acf4c 100644 --- a/packages/core-components/src/layout/BottomLink/BottomLink.tsx +++ b/packages/core-components/src/layout/BottomLink/BottomLink.tsx @@ -49,11 +49,11 @@ export const BottomLink = ({ link, title, onClick }: BottomLinkProps) => { - - - {title} - - + + + {title} + + diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx index 498b859db1..37059766a8 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.test.tsx @@ -32,7 +32,7 @@ describe('', () => { it('should render with titleComponent', async () => { const title = 'Custom title'; - const titleComponent = () =>

{title}

; + const titleComponent =

{title}

; const rendered = await renderInTestApp( , ); diff --git a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx index 4bda2d76af..2259731148 100644 --- a/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx +++ b/packages/core-components/src/layout/ContentHeader/ContentHeader.tsx @@ -18,7 +18,7 @@ * TODO favoriteable capability */ -import React, { ComponentType, PropsWithChildren } from 'react'; +import React, { PropsWithChildren, ReactNode } from 'react'; import { Typography, makeStyles } from '@material-ui/core'; import { Helmet } from 'react-helmet'; @@ -77,7 +77,7 @@ const ContentHeaderTitle = ({ type ContentHeaderProps = { title?: ContentHeaderTitleProps['title']; - titleComponent?: ComponentType; + titleComponent?: ReactNode; description?: string; textAlign?: 'left' | 'right' | 'center'; }; @@ -92,7 +92,7 @@ export const ContentHeader = ({ const classes = useStyles({ textAlign })(); const renderedTitle = TitleComponent ? ( - + TitleComponent ) : ( ); diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index e8f7d03a1d..98753ad444 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, UseFormRegisterReturn } from 'react-hook-form'; import { Typography, Button, @@ -48,12 +48,21 @@ type Data = { idToken?: string; }; +const asInputRef = (renderResult: UseFormRegisterReturn) => { + const { ref, ...rest } = renderResult; + return { + inputRef: ref, + ...rest, + }; +}; const Component: ProviderComponent = ({ onResult }) => { const classes = useFormStyles(); - const { register, handleSubmit, errors, formState } = useForm({ + const { register, handleSubmit, formState } = useForm({ mode: 'onChange', }); + const { errors } = formState; + const handleResult = ({ userId, idToken }: Data) => { onResult({ userId, @@ -76,11 +85,10 @@ const Component: ProviderComponent = ({ onResult }) => {
{errors.userId && ( {errors.userId.message} @@ -88,18 +96,19 @@ const Component: ProviderComponent = ({ onResult }) => { + !token || + ID_TOKEN_REGEX.test(token) || + 'Token is not a valid OpenID Connect JWT Token', + }), + )} label="ID Token (optional)" margin="normal" autoComplete="off" error={Boolean(errors.idToken)} - inputRef={register({ - required: false, - validate: token => - !token || - ID_TOKEN_REGEX.test(token) || - 'Token is not a valid OpenID Connect JWT Token', - })} /> {errors.idToken && ( {errors.idToken.message} diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index 2a76ab243f..4cd9b4ade7 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/core-plugin-api +## 0.1.7 + +### Patch Changes + +- 3d238b028: Migrated component data attachment method to have better compatibility with component proxies such as `react-hot-loader`. +- Updated dependencies + - @backstage/config@0.1.9 + ## 0.1.6 ### Patch Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index d482795c90..414ca2afe1 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.1.6", + "version": "0.1.7", "private": false, "publishConfig": { "access": "public", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.6", + "@backstage/config": "^0.1.9", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@types/react": "*", @@ -41,8 +41,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/core-app-api": "^0.1.8", + "@backstage/cli": "^0.7.11", + "@backstage/core-app-api": "^0.1.12", "@backstage/test-utils": "^0.1.17", "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core-plugin-api/src/extensions/componentData.test.tsx b/packages/core-plugin-api/src/extensions/componentData.test.tsx index 49a1d59cc4..6f2694a512 100644 --- a/packages/core-plugin-api/src/extensions/componentData.test.tsx +++ b/packages/core-plugin-api/src/extensions/componentData.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { attachComponentData, getComponentData } from './componentData'; -describe('elementData', () => { +describe('componentData', () => { it('should attach a single piece of data', () => { const data = { foo: 'bar' }; const Component = () => null; @@ -59,4 +59,51 @@ describe('elementData', () => { 'Attempted to attach duplicate data "my-data" to component "MyComponent"', ); }); + + describe('works across versions', () => { + it('should should be able to get data from newer versions', () => { + const data = { foo: 'bar' }; + const Component = () => null; + attachComponentData(Component, 'my-data', data); + + const element = ; + expect((element as any).type.__backstage_data.map.get('my-data')).toBe( + data, + ); + }); + + it('should should be able to attach data for newer versions', () => { + const data = { foo: 'bar' }; + const Component = () => null; + (Component as any).__backstage_data = { + map: new Map([['my-data', data]]), + }; + + const element = ; + expect(getComponentData(element, 'my-data')).toBe(data); + }); + + it('should be able to get data from older versions', () => { + const data = { foo: 'bar' }; + const Component = () => null; + attachComponentData(Component, 'my-data', data); + + const element = ; + const container = (global as any)[ + '__@backstage/component-data-store__' + ].get(element.type); + expect(container.map.get('my-data')).toBe(data); + }); + + it('should should be able to attach data for older versions', () => { + const data = { foo: 'bar' }; + const Component = () => null; + (global as any)['__@backstage/component-data-store__'].set(Component, { + map: new Map([['my-data', data]]), + }); + + const element = ; + expect(getComponentData(element, 'my-data')).toBe(data); + }); + }); }); diff --git a/packages/core-plugin-api/src/extensions/componentData.tsx b/packages/core-plugin-api/src/extensions/componentData.tsx index 85527950ab..d28d17d8ca 100644 --- a/packages/core-plugin-api/src/extensions/componentData.tsx +++ b/packages/core-plugin-api/src/extensions/componentData.tsx @@ -21,24 +21,42 @@ type DataContainer = { map: Map; }; -type MaybeComponentNode = ReactNode & { - type?: ComponentType; -}; - -// The store is bridged across versions using the global object +// This method of storing the component data was deprecated in September 2021, it +// will be removed in the future for the reasons described below. const globalStore = getOrCreateGlobalSingleton( 'component-data-store', () => new WeakMap, DataContainer>(), ); +// This key is used to attach component data to the component type (function or class) +// itself. This method is used because it has better compatibility component wrappers +// like react-hot-loader, as opposed to the WeakMap method or using a symbol. +const componentDataKey = '__backstage_data'; + +type ComponentWithData = ComponentType & { + [componentDataKey]?: DataContainer; +}; + +type MaybeComponentNode = ReactNode & { + type?: ComponentWithData; +}; + export function attachComponentData

( component: ComponentType

, type: string, data: unknown, ) { - let container = globalStore.get(component); + const dataComponent = component as ComponentWithData; + + let container = dataComponent[componentDataKey] ?? globalStore.get(component); if (!container) { container = { map: new Map() }; + Object.defineProperty(dataComponent, componentDataKey, { + enumerable: false, + configurable: true, + writable: false, + value: container, + }); globalStore.set(component, container); } @@ -65,7 +83,7 @@ export function getComponentData( return undefined; } - const container = globalStore.get(component); + const container = component[componentDataKey] ?? globalStore.get(component); if (!container) { return undefined; } diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index f283e35762..f8d93846ba 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/create-app +## 0.3.40 + +### Patch Changes + +- a5013957e: Updated the search configuration class to use the static `fromConfig`-based constructor for the `DefaultCatalogCollator`. + + To apply this change to an existing app, replace the following line in `search.ts`: + + ```diff + -collator: new DefaultCatalogCollator({ discovery }) + +collator: DefaultCatalogCollator.fromConfig(config, { discovery }) + ``` + + The `config` parameter was not needed before, so make sure you also add that in the signature of `createPlugin` + in `search.ts`: + + ```diff + export default async function createPlugin({ + logger, + discovery, + + config, + }: PluginEnvironment) { + ``` + +- Updated dependencies + - @backstage/cli-common@0.1.3 + +## 0.3.39 + +### Patch Changes + +- 25924638b: Minor tweaks to the create-app template to match earlier documented changes + ## 0.3.38 ### Patch Changes @@ -31,7 +64,7 @@ - d02768171: Updated the default create-app `EntityPage` to include orphan and processing error alerts for all entity types. Previously these were only shown for entities with the `Component` kind. This also adds the `EntityLinkCard` for API entities. - As an example, you might add this to your `packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx`: + As an example, you might add this to your `packages/app/src/components/catalog/EntityPage.tsx`: ```tsx const entityWarningContent = ( @@ -690,7 +723,7 @@ -} /> +}> + {searchPage} - +; + + ``` - Updated dependencies [9cd3c533c] diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 31ac1ba271..aac845093f 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.38", + "version": "0.3.40", "private": false, "publishConfig": { "access": "public" @@ -27,7 +27,7 @@ "start": "nodemon --" }, "dependencies": { - "@backstage/cli-common": "^0.1.2", + "@backstage/cli-common": "^0.1.3", "chalk": "^4.0.0", "commander": "^6.1.0", "fs-extra": "9.1.0", diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 288351ccdf..4cd83685a6 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -22,6 +22,7 @@ import { import { UserSettingsPage } from '@backstage/plugin-user-settings'; import { apis } from './apis'; import { entityPage } from './components/catalog/EntityPage'; +import { searchPage } from './components/search/SearchPage'; import { Root } from './components/Root'; import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; @@ -70,7 +71,9 @@ const routes = ( element={} /> } /> - } /> + }> + {searchPage} + } /> ); diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index 2ce640c124..6be2e9712d 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -1,7 +1,4 @@ -import { - DockerContainerRunner, - SingleHostDiscovery, -} from '@backstage/backend-common'; +import { DockerContainerRunner } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { createRouter } from '@backstage/plugin-scaffolder-backend'; import Docker from 'dockerode'; @@ -13,11 +10,10 @@ export default async function createPlugin({ config, database, reader, + discovery, }: PluginEnvironment): Promise { const dockerClient = new Docker(); const containerRunner = new DockerContainerRunner({ dockerClient }); - - const discovery = SingleHostDiscovery.fromConfig(config); const catalogClient = new CatalogClient({ discoveryApi: discovery }); return await createRouter({ 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 248ed37ea8..7fc317d23d 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 @@ -10,6 +10,7 @@ import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; export default async function createPlugin({ logger, discovery, + config, }: PluginEnvironment) { // Initialize a connection to a search engine. const searchEngine = new LunrSearchEngine({ logger }); @@ -19,7 +20,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { discovery }), }); // The scheduler controls when documents are gathered from collators and sent diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index a05e39a69b..b15b3437f2 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/dev-utils +## 0.2.9 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/core-components@0.4.1 + - @backstage/catalog-model@0.9.2 + - @backstage/core-app-api@0.1.12 + - @backstage/core-plugin-api@0.1.7 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/core-components@0.4.0 + - @backstage/catalog-model@0.9.1 + - @backstage/core-app-api@0.1.11 + - @backstage/integration-react@0.1.8 + ## 0.2.7 ### Patch Changes diff --git a/packages/dev-utils/api-report.md b/packages/dev-utils/api-report.md index 5f58b297d5..8d54d8f98d 100644 --- a/packages/dev-utils/api-report.md +++ b/packages/dev-utils/api-report.md @@ -7,21 +7,42 @@ import { ApiFactory } from '@backstage/core-plugin-api'; import { AppTheme } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; -import { createPlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { GridProps } from '@material-ui/core'; import { IconComponent } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; -// Warning: (ae-forgotten-export) The symbol "DevAppBuilder" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createDevApp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function createDevApp(): DevAppBuilder; -// Warning: (ae-missing-release-tag) "EntityGridItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public +export class DevAppBuilder { + addPage(opts: DevAppPageOptions): DevAppBuilder; + addRootChild(node: ReactNode): DevAppBuilder; + addThemes(themes: AppTheme[]): this; + build(): ComponentType<{}>; + registerApi< + Api, + Impl extends Api, + Deps extends { + [name in string]: unknown; + }, + >(factory: ApiFactory): DevAppBuilder; + registerPlugin(...plugins: BackstagePlugin[]): DevAppBuilder; + render(): void; +} + +// @public (undocumented) +export type DevAppPageOptions = { + path?: string; + element: JSX.Element; + children?: JSX.Element; + title?: string; + icon?: IconComponent; +}; + // @public (undocumented) export const EntityGridItem: ({ entity, diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 3990296f4c..5d6bbba3d7 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.7", + "version": "0.2.9", "private": false, "publishConfig": { "access": "public", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.9", - "@backstage/core-components": "^0.3.2", - "@backstage/core-plugin-api": "^0.1.6", - "@backstage/catalog-model": "^0.9.0", - "@backstage/integration-react": "^0.1.7", - "@backstage/plugin-catalog-react": "^0.4.3", + "@backstage/core-app-api": "^0.1.12", + "@backstage/core-components": "^0.4.1", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/catalog-model": "^0.9.2", + "@backstage/integration-react": "^0.1.8", + "@backstage/plugin-catalog-react": "^0.4.5", "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -50,7 +50,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.11", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx index 30288fe7bb..55327f35e5 100644 --- a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx +++ b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx @@ -34,6 +34,7 @@ const useStyles = makeStyles(theme => ({ }), })); +/** @public */ export const EntityGridItem = ({ entity, classes, diff --git a/packages/dev-utils/src/devApp/index.tsx b/packages/dev-utils/src/devApp/index.tsx index d72b4757c8..69394007d2 100644 --- a/packages/dev-utils/src/devApp/index.tsx +++ b/packages/dev-utils/src/devApp/index.tsx @@ -14,4 +14,5 @@ * limitations under the License. */ -export * from './render'; +export { createDevApp } from './render'; +export type { DevAppBuilder, DevAppPageOptions } from './render'; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 38cc0befe1..755289a060 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -41,10 +41,10 @@ import { attachComponentData, configApiRef, createApiFactory, - createPlugin, createRouteRef, IconComponent, RouteRef, + BackstagePlugin, } from '@backstage/core-plugin-api'; import { createApp, FlatRoutes } from '@backstage/core-app-api'; @@ -57,7 +57,8 @@ const GatheringRoute: (props: { attachComponentData(GatheringRoute, 'core.gatherMountPoints', true); -type RegisterPageOptions = { +/** @public */ +export type DevAppPageOptions = { path?: string; element: JSX.Element; children?: JSX.Element; @@ -65,14 +66,13 @@ type RegisterPageOptions = { icon?: IconComponent; }; -// TODO(rugvip): export proper plugin type from core that isn't the plugin class -type BackstagePlugin = ReturnType; - /** * DevApp builder that is similar to the App builder API, but creates an App * with the purpose of developing one or more plugins inside it. + * + * @public */ -class DevAppBuilder { +export class DevAppBuilder { private readonly plugins = new Array(); private readonly apis = new Array(); private readonly rootChildren = new Array(); @@ -118,7 +118,7 @@ class DevAppBuilder { * If no path is provided one will be generated. * If no title is provided, no sidebar item will be created. */ - addPage(opts: RegisterPageOptions): DevAppBuilder { + addPage(opts: DevAppPageOptions): DevAppBuilder { const path = opts.path ?? `/page-${this.routes.length + 1}`; if (!this.defaultPage || path === '/') { @@ -244,6 +244,8 @@ class DevAppBuilder { /** * Creates a dev app for rendering one or more plugins and exposing the touch points of the plugin. + * + * @public */ export function createDevApp() { return new DevAppBuilder(); diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts index 30d240da3d..adec0c2a42 100644 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -57,9 +57,9 @@ export default class TypeLocator { * Find exported instances and return values from calls using the types * provided in the lookup table. */ - findExportedInstances( - typeLookupTable: { [key in T]: ts.Type }, - ): { [key in T]: ExportedInstance[] } { + findExportedInstances(typeLookupTable: { + [key in T]: ts.Type; + }): { [key in T]: ExportedInstance[] } { const docMap = new Map(); for (const type of Object.values(typeLookupTable)) { docMap.set(type, []); diff --git a/packages/e2e-test/src/lib/helpers.ts b/packages/e2e-test/src/lib/helpers.ts index d0e6de2cff..2182a9d324 100644 --- a/packages/e2e-test/src/lib/helpers.ts +++ b/packages/e2e-test/src/lib/helpers.ts @@ -165,7 +165,6 @@ export async function waitForPageWithText( } catch (error) { findTextAttempts++; if (findTextAttempts <= maxFindTextAttempts) { - await browser.visit(path); await new Promise(resolve => setTimeout(resolve, intervalMs)); continue; } else { diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index e69de29bb2..7cadaa6e07 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -0,0 +1,9 @@ +# @backstage/errors + +## 0.1.2 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/config@0.1.9 diff --git a/packages/errors/api-report.md b/packages/errors/api-report.md index 6aa69592b7..019b6dd387 100644 --- a/packages/errors/api-report.md +++ b/packages/errors/api-report.md @@ -5,18 +5,12 @@ ```ts import { JsonObject } from '@backstage/config'; -// Warning: (ae-missing-release-tag) "AuthenticationError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class AuthenticationError extends CustomErrorBase {} -// Warning: (ae-missing-release-tag) "ConflictError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class ConflictError extends CustomErrorBase {} -// Warning: (ae-missing-release-tag) "CustomErrorBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class CustomErrorBase extends Error { constructor(message?: string, cause?: Error); @@ -24,15 +18,11 @@ export class CustomErrorBase extends Error { readonly cause?: Error; } -// Warning: (ae-missing-release-tag) "deserializeError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function deserializeError( data: SerializedError, ): T; -// Warning: (ae-missing-release-tag) "ErrorResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type ErrorResponse = { error: SerializedError; @@ -45,34 +35,21 @@ export type ErrorResponse = { }; }; -// Warning: (ae-missing-release-tag) "InputError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class InputError extends CustomErrorBase {} -// Warning: (ae-missing-release-tag) "NotAllowedError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class NotAllowedError extends CustomErrorBase {} -// Warning: (ae-missing-release-tag) "NotFoundError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class NotFoundError extends CustomErrorBase {} -// Warning: (ae-missing-release-tag) "NotModifiedError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class NotModifiedError extends CustomErrorBase {} -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (ae-missing-release-tag) "parseErrorResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function parseErrorResponse(response: Response): Promise; -// Warning: (ae-missing-release-tag) "ResponseError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class ResponseError extends Error { constructor(props: { @@ -87,8 +64,6 @@ export class ResponseError extends Error { readonly response: Response; } -// Warning: (ae-missing-release-tag) "SerializedError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type SerializedError = JsonObject & { name: string; @@ -97,10 +72,6 @@ export type SerializedError = JsonObject & { code?: string; }; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters -// Warning: (ae-missing-release-tag) "serializeError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function serializeError( error: Error, diff --git a/packages/errors/package.json b/packages/errors/package.json index 8fa76d8bfc..dc31d8827e 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/errors", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.2", + "@backstage/config": "^0.1.9", "cross-fetch": "^3.0.6", "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.7.0", + "@backstage/cli": "^0.7.11", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts index fc374287c7..a392135c13 100644 --- a/packages/errors/src/errors/CustomErrorBase.ts +++ b/packages/errors/src/errors/CustomErrorBase.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/** @public */ export class CustomErrorBase extends Error { readonly cause?: Error; diff --git a/packages/errors/src/errors/ResponseError.ts b/packages/errors/src/errors/ResponseError.ts index c8927a447c..f74ad2fa0c 100644 --- a/packages/errors/src/errors/ResponseError.ts +++ b/packages/errors/src/errors/ResponseError.ts @@ -24,6 +24,8 @@ import { * An error thrown as the result of a failed server request. * * The server is expected to respond on the ErrorResponse format. + * + * @public */ export class ResponseError extends Error { /** diff --git a/packages/errors/src/errors/common.ts b/packages/errors/src/errors/common.ts index 51c82eeda5..dad236d4b5 100644 --- a/packages/errors/src/errors/common.ts +++ b/packages/errors/src/errors/common.ts @@ -29,16 +29,22 @@ import { CustomErrorBase } from './CustomErrorBase'; /** * The given inputs are malformed and cannot be processed. + * + * @public */ export class InputError extends CustomErrorBase {} /** * The request requires authentication, which was not properly supplied. + * + * @public */ export class AuthenticationError extends CustomErrorBase {} /** * The authenticated caller is not allowed to perform this request. + * + * @public */ export class NotAllowedError extends CustomErrorBase {} @@ -47,16 +53,22 @@ export class NotAllowedError extends CustomErrorBase {} * * Note that this error usually is used to indicate that an entity with a given * ID does not exist, rather than signalling that an entire route is missing. + * + * @public */ export class NotFoundError extends CustomErrorBase {} /** * The request could not complete due to a conflict in the current state of the * resource. + * + * @public */ export class ConflictError extends CustomErrorBase {} /** * The requested resource has not changed since last request. + * + * @public */ export class NotModifiedError extends CustomErrorBase {} diff --git a/packages/errors/src/serialization/error.ts b/packages/errors/src/serialization/error.ts index 46a2c2f74c..ab543164d8 100644 --- a/packages/errors/src/serialization/error.ts +++ b/packages/errors/src/serialization/error.ts @@ -22,6 +22,8 @@ import { /** * The serialized form of an Error. + * + * @public */ export type SerializedError = JsonObject & { /** The name of the exception that was thrown */ @@ -37,12 +39,16 @@ export type SerializedError = JsonObject & { /** * Serializes an error object to a JSON friendly form. * - * @param error The error - * @param options.includeStackTraces: Include stack trace in the output (default false) + * @public + * @param error - The error. + * @param options - Optional serialization options. */ export function serializeError( error: Error, - options?: { includeStack?: boolean }, + options?: { + /** Include stack trace in the output (default false) */ + includeStack?: boolean; + }, ): SerializedError { const serialized = serializeErrorInternal(error); const result: SerializedError = { @@ -60,6 +66,8 @@ export function serializeError( /** * Deserializes a serialized error object back to an Error. + * + * @public */ export function deserializeError( data: SerializedError, diff --git a/packages/errors/src/serialization/response.ts b/packages/errors/src/serialization/response.ts index 0196b2d597..50bf8f7ab1 100644 --- a/packages/errors/src/serialization/response.ts +++ b/packages/errors/src/serialization/response.ts @@ -18,6 +18,8 @@ import { SerializedError } from './error'; /** * A standard shape of JSON data returned as the body of backend errors. + * + * @public */ export type ErrorResponse = { /** Details of the error that was caught */ @@ -47,7 +49,8 @@ export type ErrorResponse = { * The code is forgiving, and constructs a useful synthetic body as best it can * if the response body wasn't on the expected form. * - * @param response The response of a failed request + * @public + * @param response - The response of a failed request */ export async function parseErrorResponse( response: Response, diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 281fed0211..a9944238ce 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration-react +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.3 + - @backstage/core-components@0.4.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 3217881046..90acb9acd3 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "0.1.7", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ }, "dependencies": { "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.3.1", + "@backstage/core-components": "^0.4.0", "@backstage/core-plugin-api": "^0.1.6", - "@backstage/integration": "^0.6.0", + "@backstage/integration": "^0.6.3", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -33,8 +33,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.8", - "@backstage/dev-utils": "^0.2.6", + "@backstage/cli": "^0.7.10", + "@backstage/dev-utils": "^0.2.8", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index dfc9a5722a..1befa4f6cc 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/integration +## 0.6.3 + +### Patch Changes + +- 5dca42b17: Update to properly handle Azure DevOps Server download URL + ## 0.6.2 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index ffe4e5776f..5358912852 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.6.2", + "version": "0.6.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ "dependencies": { "@backstage/config": "^0.1.8", "cross-fetch": "^3.0.6", - "git-url-parse": "~11.4.4", + "git-url-parse": "^11.6.0", "@octokit/rest": "^18.5.3", "@octokit/auth-app": "^3.4.0", "luxon": "^2.0.2" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.10", "@backstage/config-loader": "^0.6.7", "@backstage/test-utils": "^0.1.17", "@types/jest": "^26.0.7", diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index 80cc68047f..c30283f432 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -60,22 +60,43 @@ describe('AzureIntegration', () => { expect( integration.resolveUrl({ url: '/a.yaml', - base: 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml', + base: 'https://internal.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml', lineNumber: 14, }), ).toBe( - 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml&line=14&lineEnd=15&lineStartColumn=1&lineEndColumn=1', + 'https://internal.com/organization/project/_git/repository?path=%2Fa.yaml&line=14&lineEnd=15&lineStartColumn=1&lineEndColumn=1', ); expect( integration.resolveUrl({ url: './a.yaml', - base: 'https://dev.azure.com/organization/project/_git/repository', + base: 'https://dev.azure.com/organization/_git/project', + }), + ).toBe('https://dev.azure.com/organization/_git/project?path=%2Fa.yaml'); + + expect( + integration.resolveUrl({ + url: 'https://dev.azure.com/organization/_git/project?path=%2Fa.yaml', + base: 'https://dev.azure.com/organization/_git/project', + }), + ).toBe('https://dev.azure.com/organization/_git/project?path=%2Fa.yaml'); + + expect( + integration.resolveUrl({ + url: 'https://dev.azure.com/other-organization/_git/other-project?path=%2Fa.yaml', + base: 'https://dev.azure.com/organization/_git/project', }), ).toBe( - 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', + 'https://dev.azure.com/other-organization/_git/other-project?path=%2Fa.yaml', ); + expect( + integration.resolveUrl({ + url: './a.yaml', + base: 'http://not-azure.com/organization/_git/project', + }), + ).toBe('http://not-azure.com/organization/_git/project?path=%2Fa.yaml'); + expect( integration.resolveUrl({ url: 'https://absolute.com/path', diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index c2a8dd8f5e..ab1b2489ae 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import parseGitUrl from 'git-url-parse'; import { basicIntegrations, isValidUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; +import { AzureUrl } from './AzureUrl'; import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; export class AzureIntegration implements ScmIntegration { @@ -61,29 +61,27 @@ export class AzureIntegration implements ScmIntegration { return url; } - const parsed = parseGitUrl(base); - const { organization, owner, name, filepath } = parsed; + try { + const azureUrl = AzureUrl.fromRepoUrl(base); + const newUrl = new URL(base); - // If not an actual file path within a repo, treat the URL as raw - if (!organization || !owner || !name) { + // We lean on the URL path resolution logic to resolve the path param + const mockBaseUrl = new URL(`https://a.com${azureUrl.getPath() ?? ''}`); + const updatedPath = new URL(url, mockBaseUrl).pathname; + newUrl.searchParams.set('path', updatedPath); + + if (options.lineNumber) { + newUrl.searchParams.set('line', String(options.lineNumber)); + newUrl.searchParams.set('lineEnd', String(options.lineNumber + 1)); + newUrl.searchParams.set('lineStartColumn', '1'); + newUrl.searchParams.set('lineEndColumn', '1'); + } + + return newUrl.toString(); + } catch { + // If not an actual file path within a repo, treat the URL as raw return new URL(url, base).toString(); } - - const path = filepath?.replace(/^\//, '') || ''; - const mockBaseUrl = new URL(`https://a.com/${path}`); - const updatedPath = new URL(url, mockBaseUrl).pathname; - - const newUrl = new URL(base); - newUrl.searchParams.set('path', updatedPath); - - if (options.lineNumber) { - newUrl.searchParams.set('line', String(options.lineNumber)); - newUrl.searchParams.set('lineEnd', String(options.lineNumber + 1)); - newUrl.searchParams.set('lineStartColumn', '1'); - newUrl.searchParams.set('lineEndColumn', '1'); - } - - return newUrl.toString(); } resolveEditUrl(url: string): string { diff --git a/packages/integration/src/azure/AzureUrl.test.ts b/packages/integration/src/azure/AzureUrl.test.ts new file mode 100644 index 0000000000..2f22dd13da --- /dev/null +++ b/packages/integration/src/azure/AzureUrl.test.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 { AzureUrl } from './AzureUrl'; + +describe('AzureUrl', () => { + it('should work with the short URL form', () => { + const url = AzureUrl.fromRepoUrl( + 'https://dev.azure.com/my-org/_git/my-project', + ); + + expect(url.getOwner()).toBe('my-org'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-project'); + expect(url.getRef()).toBeUndefined(); + expect(url.getPath()).toBeUndefined(); + + expect(url.toRepoUrl()).toBe( + 'https://dev.azure.com/my-org/_git/my-project', + ); + expect(() => url.toFileUrl()).toThrow( + 'Azure URL must point to a specific path to be able to download a file', + ); + expect(url.toArchiveUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?recursionLevel=full&download=true&api-version=6.0', + ); + expect(url.toCommitsUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/commits?api-version=6.0', + ); + }); + + it('should work with the short URL form with a path', () => { + const url = AzureUrl.fromRepoUrl( + 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml', + ); + + expect(url.getOwner()).toBe('my-org'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-project'); + expect(url.getRef()).toBeUndefined(); + expect(url.getPath()).toBe('/test.yaml'); + + expect(url.toRepoUrl()).toBe( + 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml', + ); + expect(url.toFileUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?api-version=6.0&path=%2Ftest.yaml', + ); + expect(url.toArchiveUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ftest.yaml', + ); + expect(url.toCommitsUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/commits?api-version=6.0', + ); + }); + + it('should work with the short URL form with a path and ref', () => { + const url = AzureUrl.fromRepoUrl( + 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GBtest-branch', + ); + + expect(url.getOwner()).toBe('my-org'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-project'); + expect(url.getRef()).toBe('test-branch'); + expect(url.getPath()).toBe('/test.yaml'); + + expect(url.toRepoUrl()).toBe( + 'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GBtest-branch', + ); + expect(url.toFileUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?api-version=6.0&path=%2Ftest.yaml&version=test-branch', + ); + expect(url.toArchiveUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ftest.yaml&version=test-branch', + ); + expect(url.toCommitsUrl()).toBe( + 'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/commits?api-version=6.0&searchCriteria.itemVersion.version=test-branch', + ); + }); + + it('should work with the long URL', () => { + const url = AzureUrl.fromRepoUrl( + 'http://my-host/my-org/my-project/_git/my-repo', + ); + + expect(url.getOwner()).toBe('my-org'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-repo'); + expect(url.getRef()).toBeUndefined(); + expect(url.getPath()).toBeUndefined(); + + expect(url.toRepoUrl()).toBe( + 'http://my-host/my-org/my-project/_git/my-repo', + ); + expect(() => url.toFileUrl()).toThrow( + 'Azure URL must point to a specific path to be able to download a file', + ); + expect(url.toArchiveUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?recursionLevel=full&download=true&api-version=6.0', + ); + expect(url.toCommitsUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/commits?api-version=6.0', + ); + }); + + it('should work with the long URL form with a path and ref', () => { + const url = AzureUrl.fromRepoUrl( + 'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch', + ); + + expect(url.getOwner()).toBe('my-org'); + expect(url.getProject()).toBe('my-project'); + expect(url.getRepo()).toBe('my-repo'); + expect(url.getRef()).toBe('test-branch'); + expect(url.getPath()).toBe('/folder'); + + expect(url.toRepoUrl()).toBe( + 'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch', + ); + expect(url.toFileUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?api-version=6.0&path=%2Ffolder&version=test-branch', + ); + expect(url.toArchiveUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ffolder&version=test-branch', + ); + expect(url.toCommitsUrl()).toBe( + 'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/commits?api-version=6.0&searchCriteria.itemVersion.version=test-branch', + ); + }); + + it('should reject non-branch refs', () => { + expect(() => + AzureUrl.fromRepoUrl( + 'https://dev.azure.com/my-org/_git/my-project?version=GC6eead79870d998a3befd4bc7c72cc89e446f2970', + ), + ).toThrow('Azure URL version must point to a git branch'); + }); + + it('should reject non-repo URLs', () => { + expect(() => + AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/_git'), + ).toThrow('Azure URL must point to a git repository'); + expect(() => + AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/_git/'), + ).toThrow('Azure URL must point to a git repository'); + expect(() => + AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/my-project/'), + ).toThrow('Azure URL must point to a git repository'); + expect(() => + AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/my-project/_not-git'), + ).toThrow('Azure URL must point to a git repository'); + expect(() => + AzureUrl.fromRepoUrl( + 'https://dev.azure.com/my-org/my-project/_not-git/my-repo', + ), + ).toThrow('Azure URL must point to a git repository'); + expect(() => + AzureUrl.fromRepoUrl( + 'https://dev.azure.com/my-org/_workitems/recentlyupdated/', + ), + ).toThrow('Azure URL must point to a git repository'); + }); +}); diff --git a/packages/integration/src/azure/AzureUrl.ts b/packages/integration/src/azure/AzureUrl.ts new file mode 100644 index 0000000000..76de8550ed --- /dev/null +++ b/packages/integration/src/azure/AzureUrl.ts @@ -0,0 +1,231 @@ +/* + * 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. + */ + +const VERSION_PREFIX_GIT_BRANCH = 'GB'; + +export class AzureUrl { + /** + * Parses an azure URL as copied from the browser address bar. + * + * Throws an error if the URL is not a valid azure repo URL. + */ + static fromRepoUrl(repoUrl: string): AzureUrl { + const url = new URL(repoUrl); + + let owner; + let project; + let repo; + + const parts = url.pathname.split('/').map(part => decodeURIComponent(part)); + if (parts[2] === '_git') { + owner = parts[1]; + project = repo = parts[3]; + } else if (parts[3] === '_git') { + owner = parts[1]; + project = parts[2]; + repo = parts[4]; + } + + if (!owner || !project || !repo) { + throw new Error('Azure URL must point to a git repository'); + } + + const path = url.searchParams.get('path') ?? undefined; + + let ref; + const version = url.searchParams.get('version'); + if (version) { + const prefix = version.slice(0, 2); + if (prefix !== 'GB') { + throw new Error('Azure URL version must point to a git branch'); + } + ref = version.slice(2); + } + + return new AzureUrl(url.origin, owner, project, repo, path, ref); + } + + #origin: string; + #owner: string; + #project: string; + #repo: string; + #path?: string; + #ref?: string; + + private constructor( + origin: string, + owner: string, + project: string, + repo: string, + path?: string, + ref?: string, + ) { + this.#origin = origin; + this.#owner = owner; + this.#project = project; + this.#repo = repo; + this.#path = path; + this.#ref = ref; + } + + #baseUrl = (...parts: string[]): URL => { + const url = new URL(this.#origin); + url.pathname = parts.map(part => encodeURIComponent(part)).join('/'); + return url; + }; + + /** + * Returns a repo URL that can be used to navigate to the resource in azure. + * + * Throws an error if the URL is not a valid azure repo URL. + */ + toRepoUrl(): string { + let url; + if (this.#project === this.#repo) { + url = this.#baseUrl(this.#owner, '_git', this.#repo); + } else { + url = this.#baseUrl(this.#owner, this.#project, '_git', this.#repo); + } + + if (this.#path) { + url.searchParams.set('path', this.#path); + } + if (this.#ref) { + url.searchParams.set('version', VERSION_PREFIX_GIT_BRANCH + this.#ref); + } + + return url.toString(); + } + + /** + * Returns the file download URL for this azure resource. + * + * Throws an error if the URL does not point to a file. + */ + toFileUrl(): string { + if (!this.#path) { + throw new Error( + 'Azure URL must point to a specific path to be able to download a file', + ); + } + + const url = this.#baseUrl( + this.#owner, + this.#project, + '_apis', + 'git', + 'repositories', + this.#repo, + 'items', + ); + url.searchParams.set('api-version', '6.0'); + url.searchParams.set('path', this.#path); + + if (this.#ref) { + url.searchParams.set('version', this.#ref); + } + + return url.toString(); + } + + /** + * Returns the archive download URL for this azure resource. + * + * Throws an error if the URL does not point to a repo. + */ + toArchiveUrl(): string { + const url = this.#baseUrl( + this.#owner, + this.#project, + '_apis', + 'git', + 'repositories', + this.#repo, + 'items', + ); + url.searchParams.set('recursionLevel', 'full'); + url.searchParams.set('download', 'true'); + url.searchParams.set('api-version', '6.0'); + + if (this.#path) { + url.searchParams.set('scopePath', this.#path); + } + if (this.#ref) { + url.searchParams.set('version', this.#ref); + } + + return url.toString(); + } + + /** + * Returns the API url for fetching commits from a branch for this azure resource. + * + * Throws an error if the URL does not point to a commit. + */ + toCommitsUrl(): string { + const url = this.#baseUrl( + this.#owner, + this.#project, + '_apis', + 'git', + 'repositories', + this.#repo, + 'commits', + ); + url.searchParams.set('api-version', '6.0'); + + if (this.#ref) { + url.searchParams.set('searchCriteria.itemVersion.version', this.#ref); + } + + return url.toString(); + } + + /** + * Returns the name of the owner, a user or an organization. + */ + getOwner(): string { + return this.#owner; + } + + /** + * Returns the name of the project. + */ + getProject(): string { + return this.#project; + } + + /** + * Returns the name of the repo. + */ + getRepo(): string { + return this.#repo; + } + + /** + * Returns the file path within the repo if the URL contains one. + */ + getPath(): string | undefined { + return this.#path; + } + + /** + * Returns the git ref in the repo if the URL contains one. + */ + getRef(): string | undefined { + return this.#ref; + } +} diff --git a/packages/integration/src/azure/core.test.ts b/packages/integration/src/azure/core.test.ts index 9937008b61..ad8f2691b5 100644 --- a/packages/integration/src/azure/core.test.ts +++ b/packages/integration/src/azure/core.test.ts @@ -45,22 +45,22 @@ describe('azure core', () => { { url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', result: - 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master', }, { url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', result: - 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml', }, { url: 'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml', result: - 'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', + 'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml', }, { url: 'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', result: - 'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + 'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master', }, ])('should handle happy path %#', async ({ url, result }) => { expect(getAzureFileFetchUrl(url)).toBe(result); @@ -69,13 +69,11 @@ describe('azure core', () => { it.each([ { url: 'https://api.com/a/b/blob/master/path/to/c.yaml', - error: - 'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + error: 'Azure URL must point to a git repository', }, { url: 'com/a/b/blob/master/path/to/c.yaml', - error: - 'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + error: 'Invalid URL: com/a/b/blob/master/path/to/c.yaml', }, ])('should handle error path %#', ({ url, error }) => { expect(() => getAzureFileFetchUrl(url)).toThrow(error); @@ -95,7 +93,22 @@ describe('azure core', () => { const result = getAzureDownloadUrl( 'https://dev.azure.com/organization/project/_git/repository?path=%2Fdocs', ); - expect(new URL(result).searchParams.get('scopePath')).toEqual('docs'); + expect(new URL(result).searchParams.get('scopePath')).toEqual('/docs'); + }); + + it.each([ + { + url: 'https://dev.azure.com/org-name/project-name/_git/repo-name', + result: + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?recursionLevel=full&download=true&api-version=6.0', + }, + { + url: 'https://api.com/org-name/project-name/_git/repo-name', + result: + 'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?recursionLevel=full&download=true&api-version=6.0', + }, + ])('should handle happy path %#', async ({ url, result }) => { + expect(getAzureDownloadUrl(url)).toBe(result); }); }); }); diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 18de5aa1d2..30603b1f09 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import parseGitUrl from 'git-url-parse'; +import { AzureUrl } from './AzureUrl'; import { AzureIntegrationConfig } from './config'; /** @@ -28,53 +28,7 @@ import { AzureIntegrationConfig } from './config'; * @param url A URL pointing to a file */ export function getAzureFileFetchUrl(url: string): string { - try { - const parsedUrl = new URL(url); - - const [empty, userOrOrg, project, srcKeyword, repoName] = - parsedUrl.pathname.split('/'); - - const path = parsedUrl.searchParams.get('path') || ''; - const ref = parsedUrl.searchParams.get('version')?.substr(2); - - if ( - empty !== '' || - userOrOrg === '' || - project === '' || - srcKeyword !== '_git' || - repoName === '' || - path === '' || - ref === '' - ) { - throw new Error('Wrong Azure Devops URL or Invalid file path'); - } - - // transform to api - parsedUrl.pathname = [ - empty, - userOrOrg, - project, - '_apis', - 'git', - 'repositories', - repoName, - 'items', - ].join('/'); - - const queryParams = [`path=${path}`]; - - if (ref) { - queryParams.push(`version=${ref}`); - } - - parsedUrl.search = queryParams.join('&'); - - parsedUrl.protocol = 'https'; - - return parsedUrl.toString(); - } catch (e) { - throw new Error(`Incorrect URL: ${url}, ${e}`); - } + return AzureUrl.fromRepoUrl(url).toFileUrl(); } /** @@ -84,23 +38,7 @@ export function getAzureFileFetchUrl(url: string): string { * @param url A URL pointing to a path */ export function getAzureDownloadUrl(url: string): string { - const { - name: repoName, - owner: project, - organization, - protocol, - resource, - filepath, - } = parseGitUrl(url); - - // scopePath will limit the downloaded content - // /docs will only download the docs folder and everything below it - // /docs/index.md will only download index.md but put it in the root of the archive - const scopePath = filepath - ? `&scopePath=${encodeURIComponent(filepath)}` - : ''; - - return `${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`; + return AzureUrl.fromRepoUrl(url).toArchiveUrl(); } /** @@ -109,49 +47,7 @@ export function getAzureDownloadUrl(url: string): string { * @param url A URL pointing to a repository or a sub-path */ export function getAzureCommitsUrl(url: string): string { - try { - const parsedUrl = new URL(url); - - const [empty, userOrOrg, project, srcKeyword, repoName] = - parsedUrl.pathname.split('/'); - - // Remove the "GB" from "GBmain" for example. - const ref = parsedUrl.searchParams.get('version')?.substr(2); - - if ( - !!empty || - !userOrOrg || - !project || - srcKeyword !== '_git' || - !repoName - ) { - throw new Error('Wrong Azure Devops URL'); - } - - // transform to commits api - parsedUrl.pathname = [ - empty, - userOrOrg, - project, - '_apis', - 'git', - 'repositories', - repoName, - 'commits', - ].join('/'); - - const queryParams = []; - if (ref) { - queryParams.push(`searchCriteria.itemVersion.version=${ref}`); - } - parsedUrl.search = queryParams.join('&'); - - parsedUrl.protocol = 'https'; - - return parsedUrl.toString(); - } catch (e) { - throw new Error(`Incorrect URL: ${url}, ${e}`); - } + return AzureUrl.fromRepoUrl(url).toCommitsUrl(); } /** diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index c7ba6408d9..3156488ba7 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -110,9 +110,10 @@ class GithubAppManager { const installationClient = new Octokit({ auth: result.data.token, }); - const repos = - await installationClient.apps.listReposAccessibleToInstallation(); - const hasRepo = repos.data.repositories.some(repository => { + const repos = await installationClient.paginate( + installationClient.apps.listReposAccessibleToInstallation, + ); + const hasRepo = repos.some(repository => { return repository.name === repo; }); if (!hasRepo) { diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index f7d12eb006..34f956586e 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/search-common +## 0.2.0 + +### Minor Changes + +- a13f21cdc: Implement optional `pageCursor` based paging in search. + + To use paging in your app, add a `` to your + `SearchPage.tsx`. + ## 0.1.3 ### Patch Changes diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index 055f2d152f..f00beb6830 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -53,7 +53,7 @@ export interface SearchQuery { // (undocumented) filters?: JsonObject; // (undocumented) - pageCursor: string; + pageCursor?: string; // (undocumented) term: string; // (undocumented) @@ -74,6 +74,10 @@ export interface SearchResult { // // @public (undocumented) export interface SearchResultSet { + // (undocumented) + nextPageCursor?: string; + // (undocumented) + previousPageCursor?: string; // (undocumented) results: SearchResult[]; } diff --git a/packages/search-common/package.json b/packages/search-common/package.json index ede7baeae2..be1aad16b0 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.1.3", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -39,7 +39,7 @@ "@backstage/config": "^0.1.6" }, "devDependencies": { - "@backstage/cli": "^0.7.7" + "@backstage/cli": "^0.7.10" }, "jest": { "roots": [ diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 2a4447efac..063e0cb96c 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -19,7 +19,7 @@ export interface SearchQuery { term: string; filters?: JsonObject; types?: string[]; - pageCursor: string; + pageCursor?: string; } export interface SearchResult { @@ -29,6 +29,8 @@ export interface SearchResult { export interface SearchResultSet { results: SearchResult[]; + nextPageCursor?: string; + previousPageCursor?: string; } /** diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 342bb76c21..360bebded0 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/techdocs-common +## 0.10.0 + +### Minor Changes + +- 8b0f6f860: Set the correct `edit_uri` or `repo_url` for documentation pages that are hosted on GitHub and GitLab. + + The constructor of the `TechDocsGenerator` changed. + Prefer the use of `TechdocsGenerator.fromConfig(…)` instead: + + ```diff + - const techdocsGenerator = new TechdocsGenerator({ + + const techdocsGenerator = TechdocsGenerator.fromConfig(config, { + logger, + containerRunner, + - config, + }); + ``` + +### Patch Changes + +- 30ed662a3: Adding in-context search to TechDocs Reader component. Using existing search-backend to query for indexed search results scoped into a specific entity's techdocs. Needs TechDocsCollator enabled on the backend to work. + + Adding extra information to indexed tech docs documents for search. + +- 3624616e7: "Local" (out-of-the-box) publisher explicitly follows lower-case entity triplet + logic. +- 67ba7e088: Only write the updated `mkdocs.yml` file if the content was updated. + + This keeps local files unchanged if the `dir` annotation is used in combination with the `file` location. + +- 8eab6be6a: Force using `posix` path for cloud storage +- Updated dependencies + - @backstage/integration@0.6.3 + - @backstage/search-common@0.2.0 + - @backstage/catalog-model@0.9.1 + - @backstage/backend-common@0.9.1 + ## 0.9.0 ### Minor Changes diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 0c7e5b03c7..edf4bac6db 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -10,6 +10,7 @@ import { ContainerRunner } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import express from 'express'; +import { IndexableDocument } from '@backstage/search-common'; import { Logger as Logger_2 } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -222,6 +223,24 @@ export type PublisherType = // @public (undocumented) export type RemoteProtocol = 'url' | 'dir'; +// Warning: (ae-missing-release-tag) "TechDocsDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface TechDocsDocument extends IndexableDocument { + // (undocumented) + kind: string; + // (undocumented) + lifecycle: string; + // (undocumented) + name: string; + // (undocumented) + namespace: string; + // (undocumented) + owner: string; + // (undocumented) + path: string; +} + // Warning: (ae-missing-release-tag) "TechdocsGenerator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -230,10 +249,12 @@ export class TechdocsGenerator implements GeneratorBase { logger, containerRunner, config, + scmIntegrations, }: { logger: Logger_2; containerRunner: ContainerRunner; config: Config; + scmIntegrations: ScmIntegrationRegistry; }); // (undocumented) static fromConfig( @@ -245,7 +266,7 @@ export class TechdocsGenerator implements GeneratorBase { containerRunner: ContainerRunner; logger: Logger_2; }, - ): Promise; + ): TechdocsGenerator; // (undocumented) run({ inputDir, diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index f529d93ca6..2316d96a55 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.9.0", + "version": "0.10.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,17 +38,19 @@ "dependencies": { "@azure/identity": "^1.5.0", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.9.0", - "@backstage/catalog-model": "^0.9.0", + "@backstage/backend-common": "^0.9.1", + "@backstage/catalog-model": "^0.9.1", "@backstage/config": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.2", + "@backstage/search-common": "^0.2.0", + "@backstage/integration": "^0.6.3", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.4", "@types/express": "^4.17.6", "aws-sdk": "^2.840.0", "express": "^4.17.1", "fs-extra": "9.1.0", + "git-url-parse": "^11.6.0", "js-yaml": "^4.0.0", "json5": "^2.1.3", "mime-types": "^2.1.27", @@ -58,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.10", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/techdocs-common/src/index.ts b/packages/techdocs-common/src/index.ts index a9a254a1ee..faf60440c0 100644 --- a/packages/techdocs-common/src/index.ts +++ b/packages/techdocs-common/src/index.ts @@ -15,3 +15,4 @@ */ export * from './stages'; export * from './helpers'; +export * from './techdocsTypes'; diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml new file mode 100644 index 0000000000..3924224560 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml @@ -0,0 +1,3 @@ +site_name: Test site name +site_description: Test site description +# This is a comment that is removed after editing diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml new file mode 100644 index 0000000000..c7f402ec12 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml @@ -0,0 +1,4 @@ +site_name: Test site name +site_description: Test site description + +edit_uri: https://github.com/backstage/backstage/edit/main/docs diff --git a/packages/techdocs-common/src/stages/generate/generators.test.ts b/packages/techdocs-common/src/stages/generate/generators.test.ts index a2ace2628c..56343763e9 100644 --- a/packages/techdocs-common/src/stages/generate/generators.test.ts +++ b/packages/techdocs-common/src/stages/generate/generators.test.ts @@ -44,10 +44,9 @@ describe('generators', () => { it('should return correct registered generator', async () => { const generators = new Generators(); - const techdocs = new TechdocsGenerator({ + const techdocs = TechdocsGenerator.fromConfig(new ConfigReader({}), { logger, containerRunner, - config: new ConfigReader({}), }); generators.register('techdocs', techdocs); diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/packages/techdocs-common/src/stages/generate/generators.ts index af7958dcfb..3d1ad2b73b 100644 --- a/packages/techdocs-common/src/stages/generate/generators.ts +++ b/packages/techdocs-common/src/stages/generate/generators.ts @@ -38,10 +38,9 @@ export class Generators implements GeneratorBuilder { ): Promise { const generators = new Generators(); - const techdocsGenerator = new TechdocsGenerator({ + const techdocsGenerator = TechdocsGenerator.fromConfig(config, { logger, containerRunner, - config, }); generators.register('techdocs', techdocsGenerator); diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 4039d340f2..fcf506098e 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import os from 'os'; import path, { resolve as resolvePath } from 'path'; import { ParsedLocationAnnotation } from '../../helpers'; -import { RemoteProtocol } from '../prepare/types'; import { addBuildTimestampMetadata, getGeneratorKey, getMkdocsYml, getRepoUrlFromLocationAnnotation, - isValidRepoUrlForMkdocs, patchMkdocsYmlPreBuild, storeEtagMetadata, validateMkdocsYaml, @@ -48,6 +49,9 @@ const mkdocsYmlWithExtensions = fs.readFileSync( const mkdocsYmlWithRepoUrl = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'), ); +const mkdocsYmlWithEditUri = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_edit_uri.yml'), +); const mkdocsYmlWithValidDocDir = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_valid_doc_dir.yml'), ); @@ -57,9 +61,14 @@ const mkdocsYmlWithInvalidDocDir = fs.readFileSync( const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir2.yml'), ); +const mkdocsYmlWithComments = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'), +); const mockLogger = getVoidLogger(); const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({})); + describe('helpers', () => { describe('getGeneratorKey', () => { it('should return techdocs as the only generator key', () => { @@ -68,120 +77,85 @@ describe('helpers', () => { }); }); - describe('isValidRepoUrlForMkdocs', () => { - it('should return true for valid repo_url values for mkdocs', () => { - const validRepoUrls = [ - 'https://github.com/org/repo', - 'https://github.com/backstage/backstage/', - 'https://github.com/org123/repo1-2-3/', - 'http://github.com/insecureOrg/insecureRepo', - 'https://gitlab.com/org/repo', - 'https://gitlab.com/backstage/backstage/', - 'https://gitlab.com/org123/repo1-2-3/', - 'http://gitlab.com/insecureOrg/insecureRepo', - ]; - - const validRemoteProtocols = ['github', 'gitlab']; - - validRepoUrls.forEach(url => { - validRemoteProtocols.forEach(targetType => { - expect( - isValidRepoUrlForMkdocs(url, targetType as RemoteProtocol), - ).toBe(true); - }); - }); - }); - - it('should return false for invalid repo_urls values for mkdocs', () => { - const invalidRepoUrls = [ - 'git@github.com:org/repo', - 'https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend', - ]; - - invalidRepoUrls.forEach(url => { - expect(isValidRepoUrlForMkdocs(url, 'github')).toBe(false); - }); - }); - - it('should return false for unsupported remote protocols', () => { - const validRepoUrl = 'https://github.com/backstage/backstage'; - - const unsupportedRemoteProtocols = ['dir', 'file', 'url']; - - unsupportedRemoteProtocols.forEach(targetType => { - expect( - isValidRepoUrlForMkdocs(validRepoUrl, targetType as RemoteProtocol), - ).toBe(false); - }); - }); - }); - describe('getRepoUrlFromLocationAnnotation', () => { - it('should return undefined for unsupported location type', () => { - const parsedLocationAnnotation1: ParsedLocationAnnotation = { + it.each` + url | repo_url | edit_uri + ${'https://github.com/backstage/backstage'} | ${'https://github.com/backstage/backstage'} | ${undefined} + ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${undefined} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/docs'} + ${'https://github.com/backstage/backstage/tree/main/'} | ${undefined} | ${'https://github.com/backstage/backstage/edit/main/docs'} + ${'https://gitlab.com/backstage/backstage'} | ${'https://gitlab.com/backstage/backstage'} | ${undefined} + ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${undefined} | ${'https://gitlab.com/backstage/backstage/-/edit/main/examples/techdocs/docs'} + ${'https://gitlab.com/backstage/backstage/-/blob/main/'} | ${undefined} | ${'https://gitlab.com/backstage/backstage/-/edit/main/docs'} + `('should convert $url', ({ url: target, repo_url, edit_uri }) => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target, + }; + + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + ), + ).toEqual({ repo_url, edit_uri }); + }); + + it.each` + url | edit_uri + ${'https://github.com/backstage/backstage/tree/main/examples/techdocs/'} | ${'https://github.com/backstage/backstage/edit/main/examples/techdocs/custom/folder'} + ${'https://github.com/backstage/backstage/tree/main/'} | ${'https://github.com/backstage/backstage/edit/main/custom/folder'} + ${'https://gitlab.com/backstage/backstage/-/blob/main/examples/techdocs/'} | ${'https://gitlab.com/backstage/backstage/-/edit/main/examples/techdocs/custom/folder'} + ${'https://gitlab.com/backstage/backstage/-/blob/main/'} | ${'https://gitlab.com/backstage/backstage/-/edit/main/custom/folder'} + `( + 'should convert $url with custom docsFolder', + ({ url: target, edit_uri }) => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target, + }; + + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + './custom/folder', + ), + ).toEqual({ edit_uri }); + }, + ); + + it.each` + url + ${'https://bitbucket.org/backstage/backstage/src/master/examples/techdocs/'} + ${'https://bitbucket.org/backstage/backstage/src/master/'} + ${'https://dev.azure.com/organization/project/_git/repository?path=%2Fexamples%2Ftechdocs'} + ${'https://dev.azure.com/organization/project/_git/repository?path=%2F'} + `('should ignore $url', ({ url: target }) => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target, + }; + + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + ), + ).toEqual({}); + }); + + it('should ignore unsupported location type', () => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { type: 'dir', target: '/home/user/workspace/docs-repository', }; - const parsedLocationAnnotation2: ParsedLocationAnnotation = { - type: 'file' as RemoteProtocol, - target: '/home/user/workspace/docs-repository/catalog-info.yaml', - }; - - const parsedLocationAnnotation3: ParsedLocationAnnotation = { - type: 'url', - target: 'https://my-website.com/storage/this/docs/repository', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation1)).toBe( - undefined, - ); - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation2)).toBe( - undefined, - ); - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation3)).toBe( - undefined, - ); - }); - - it('should return correct target url for supported hosts', () => { - const parsedLocationAnnotation1: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, - target: 'https://github.com/backstage/backstage.git', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation1)).toBe( - 'https://github.com/backstage/backstage', - ); - - const parsedLocationAnnotation2: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, - target: 'https://github.com/org/repo', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation2)).toBe( - 'https://github.com/org/repo', - ); - - const parsedLocationAnnotation3: ParsedLocationAnnotation = { - type: 'gitlab' as RemoteProtocol, - target: 'https://gitlab.com/org/repo', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation3)).toBe( - 'https://gitlab.com/org/repo', - ); - - const parsedLocationAnnotation4: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, - target: - 'github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', - }; - - expect(getRepoUrlFromLocationAnnotation(parsedLocationAnnotation4)).toBe( - 'github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component', - ); + expect( + getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + ), + ).toEqual({}); }); }); @@ -190,7 +164,9 @@ describe('helpers', () => { mockFs({ '/mkdocs.yml': mkdocsYml, '/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl, + '/mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri, '/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions, + '/mkdocs_with_comments.yml': mkdocsYmlWithComments, }); }); @@ -198,9 +174,9 @@ describe('helpers', () => { mockFs.restore(); }); - it('should add repo_url to mkdocs.yml', async () => { + it('should add edit_uri to mkdocs.yml', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, + type: 'url', target: 'https://github.com/backstage/backstage', }; @@ -208,6 +184,7 @@ describe('helpers', () => { '/mkdocs.yml', mockLogger, parsedLocationAnnotation, + scmIntegrations, ); const updatedMkdocsYml = await fs.readFile('/mkdocs.yml'); @@ -219,7 +196,7 @@ describe('helpers', () => { it('should add repo_url to mkdocs.yml that contains custom yaml tags', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, + type: 'url', target: 'https://github.com/backstage/backstage', }; @@ -227,6 +204,7 @@ describe('helpers', () => { '/mkdocs_with_extensions.yml', mockLogger, parsedLocationAnnotation, + scmIntegrations, ); const updatedMkdocsYml = await fs.readFile('/mkdocs_with_extensions.yml'); @@ -241,7 +219,7 @@ describe('helpers', () => { it('should not override existing repo_url in mkdocs.yml', async () => { const parsedLocationAnnotation: ParsedLocationAnnotation = { - type: 'github' as RemoteProtocol, + type: 'url', target: 'https://github.com/neworg/newrepo', }; @@ -249,6 +227,7 @@ describe('helpers', () => { '/mkdocs_with_repo_url.yml', mockLogger, parsedLocationAnnotation, + scmIntegrations, ); const updatedMkdocsYml = await fs.readFile('/mkdocs_with_repo_url.yml'); @@ -260,6 +239,51 @@ describe('helpers', () => { 'repo_url: https://github.com/neworg/newrepo', ); }); + + it('should not override existing edit_uri in mkdocs.yml', async () => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'url', + target: 'https://github.com/neworg/newrepo', + }; + + await patchMkdocsYmlPreBuild( + '/mkdocs_with_edit_uri.yml', + mockLogger, + parsedLocationAnnotation, + scmIntegrations, + ); + + const updatedMkdocsYml = await fs.readFile('/mkdocs_with_edit_uri.yml'); + + expect(updatedMkdocsYml.toString()).toContain( + 'edit_uri: https://github.com/backstage/backstage/edit/main/docs', + ); + expect(updatedMkdocsYml.toString()).not.toContain( + 'https://github.com/neworg/newrepo', + ); + }); + + it('should not update mkdocs.yml if nothing should be changed', async () => { + const parsedLocationAnnotation: ParsedLocationAnnotation = { + type: 'dir', + target: '/unsupported/path', + }; + + await patchMkdocsYmlPreBuild( + '/mkdocs_with_comments.yml', + mockLogger, + parsedLocationAnnotation, + scmIntegrations, + ); + + const updatedMkdocsYml = await fs.readFile('/mkdocs_with_comments.yml'); + + expect(updatedMkdocsYml.toString()).toContain( + '# This is a comment that is removed after editing', + ); + expect(updatedMkdocsYml.toString()).not.toContain('edit_uri'); + expect(updatedMkdocsYml.toString()).not.toContain('repo_url'); + }); }); describe('addBuildTimestampMetadata', () => { diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 7c60c7667f..0ac05e7dd2 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -14,10 +14,12 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { isChildPath } from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ScmIntegrationRegistry } from '@backstage/integration'; import { spawn } from 'child_process'; import fs from 'fs-extra'; +import gitUrlParse from 'git-url-parse'; import yaml, { DEFAULT_SCHEMA, Type } from 'js-yaml'; import path, { resolve as resolvePath } from 'path'; import { PassThrough, Writable } from 'stream'; @@ -80,63 +82,42 @@ export const runCommand = async ({ }; /** - * Return true if mkdocs can compile docs with provided repo_url - * - * Valid repo_url examples in mkdocs.yml - * - https://github.com/backstage/backstage - * - https://gitlab.com/org/repo/ - * - http://github.com/backstage/backstage - * - A http(s) protocol URL to the root of the repository - * - * Invalid repo_url examples in mkdocs.yml - * - https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component - * - (anything that is not valid as described above) - * - * @param {string} repoUrl URL supposed to be used as repo_url in mkdocs.yml - * @param {string} locationType Type of source code host - github, gitlab, dir, url, etc. - * @returns {boolean} - */ -export const isValidRepoUrlForMkdocs = ( - repoUrl: string, - locationType: string, -): boolean => { - // Trim trailing slash - const cleanRepoUrl = repoUrl.replace(/\/$/, ''); - - if (locationType === 'github' || locationType === 'gitlab') { - // A valid repoUrl to the root of the repository will be split into 5 strings if split using the / delimiter. - // We do not want URLs which have more than that number of forward slashes since they will signify a non-root location - // Note: This is not the best possible implementation but will work most of the times.. Feel free to improve or - // highlight edge cases. - return cleanRepoUrl.split('/').length === 5; - } - - return false; -}; - -/** - * Return a valid URL of the repository used in backstage.io/techdocs-ref annotation. - * Return undefined if the `target` is not valid in context of repo_url in mkdocs.yml - * Alter URL so that it is a valid repo_url config in mkdocs.yml + * Return the source url for MkDocs based on the backstage.io/techdocs-ref annotation. + * Depending on the type of target, it can either return a repo_url, an edit_uri, both, or none. * * @param {ParsedLocationAnnotation} parsedLocationAnnotation Object with location url and type - * @returns {string | undefined} + * @param {ScmIntegrationRegistry} scmIntegrations the scmIntegration to do url transformations + * @param {string} docsFolder the configured docs folder in the mkdocs.yml (defaults to 'docs') + * @returns the settings for the mkdocs.yml */ export const getRepoUrlFromLocationAnnotation = ( parsedLocationAnnotation: ParsedLocationAnnotation, -): string | undefined => { + scmIntegrations: ScmIntegrationRegistry, + docsFolder: string = 'docs', +): { repo_url?: string; edit_uri?: string } => { const { type: locationType, target } = parsedLocationAnnotation; - // Add more options from the RemoteProtocol type of parsedLocationAnnotation.type here - // when TechDocs supports more hosts and if mkdocs can generated an Edit URL for them. - const supportedHosts = ['github', 'gitlab']; + if (locationType === 'url') { + const integration = scmIntegrations.byUrl(target); - if (supportedHosts.includes(locationType)) { - // Trim .git or .git/ from the end of repository url - return target.replace(/.git\/*$/, ''); + // We only support it for github and gitlab for now as the edit_uri + // is not properly supported for others yet. + if (integration && ['github', 'gitlab'].includes(integration.type)) { + // handle the case where a user manually writes url:https://github.com/backstage/backstage i.e. without /blob/... + const { filepathtype } = gitUrlParse(target); + if (filepathtype === '') { + return { repo_url: target }; + } + + const sourceFolder = integration.resolveUrl({ + url: `./${docsFolder}`, + base: target, + }); + return { edit_uri: integration.resolveEditUrl(sourceFolder) }; + } } - return undefined; + return {}; }; class UnknownTag { @@ -217,7 +198,7 @@ export const validateMkdocsYaml = async ( * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. * * List of tasks: - * - Add repo_url if it does not exists + * - Add repo_url or edit_uri if it does not exists * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get * the repository URL. @@ -228,12 +209,18 @@ export const validateMkdocsYaml = async ( * @param {string} mkdocsYmlPath Absolute path to mkdocs.yml or equivalent of a docs site * @param {Logger} logger * @param {ParsedLocationAnnotation} parsedLocationAnnotation Object with location url and type + * @param {ScmIntegrationRegistry} scmIntegrations the scmIntegration to do url transformations */ export const patchMkdocsYmlPreBuild = async ( mkdocsYmlPath: string, logger: Logger, parsedLocationAnnotation: ParsedLocationAnnotation, + scmIntegrations: ScmIntegrationRegistry, ) => { + // We only want to override the mkdocs.yml if it has actually changed. This is relevant if + // used with a 'dir' location on the file system as this would permanently update the file. + let didEdit = false; + let mkdocsYmlFileString; try { mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); @@ -260,25 +247,37 @@ export const patchMkdocsYmlPreBuild = async ( return; } - // Add repo_url to mkdocs.yml if it is missing. This will enable the Page edit button generated by MkDocs. - if (!('repo_url' in mkdocsYml)) { - const repoUrl = getRepoUrlFromLocationAnnotation(parsedLocationAnnotation); - if (repoUrl !== undefined) { - // mkdocs.yml will not build with invalid repo_url. So, make sure it is valid. - // TODO: this is no longer working/meaningful because annotation type is - // now only ever "url" or "dir." Should be re-implemented! - if (isValidRepoUrlForMkdocs(repoUrl, parsedLocationAnnotation.type)) { - mkdocsYml.repo_url = repoUrl; - } + // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. + // This will enable the Page edit button generated by MkDocs. + // If the either has been set, keep the original value + if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { + const result = getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + mkdocsYml.docs_dir, + ); + + if (result.repo_url || result.edit_uri) { + mkdocsYml.repo_url = result.repo_url; + mkdocsYml.edit_uri = result.edit_uri; + didEdit = true; + + logger.info( + `Set ${JSON.stringify( + result, + )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, + ); } } try { - await fs.writeFile( - mkdocsYmlPath, - yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), - 'utf8', - ); + if (didEdit) { + await fs.writeFile( + mkdocsYmlPath, + yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), + 'utf8', + ); + } } catch (error) { logger.warn( `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 1a6577cad9..c3d48507ce 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -18,6 +18,10 @@ import { ContainerRunner } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import path from 'path'; import { Logger } from 'winston'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import { addBuildTimestampMetadata, getMkdocsYml, @@ -39,29 +43,39 @@ export class TechdocsGenerator implements GeneratorBase { private readonly logger: Logger; private readonly containerRunner: ContainerRunner; private readonly options: GeneratorConfig; + private readonly scmIntegrations: ScmIntegrationRegistry; - static async fromConfig( + static fromConfig( config: Config, { containerRunner, logger, }: { containerRunner: ContainerRunner; logger: Logger }, ) { - return new TechdocsGenerator({ logger, containerRunner, config }); + const scmIntegrations = ScmIntegrations.fromConfig(config); + return new TechdocsGenerator({ + logger, + containerRunner, + config, + scmIntegrations, + }); } constructor({ logger, containerRunner, config, + scmIntegrations, }: { logger: Logger; containerRunner: ContainerRunner; config: Config; + scmIntegrations: ScmIntegrationRegistry; }) { this.logger = logger; this.options = readGeneratorConfig(config, logger); this.containerRunner = containerRunner; + this.scmIntegrations = scmIntegrations; } public async run({ @@ -74,16 +88,19 @@ export class TechdocsGenerator implements GeneratorBase { }: GeneratorRunOptions): Promise { // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir); + + // validate the docs_dir first + await validateMkdocsYaml(inputDir, content); + if (parsedLocationAnnotation) { await patchMkdocsYmlPreBuild( mkdocsYmlPath, childLogger, parsedLocationAnnotation, + this.scmIntegrations, ); } - await validateMkdocsYaml(inputDir, content); - // Directories to bind on container const mountDirs = { [inputDir]: '/input', diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/packages/techdocs-common/src/stages/publish/local.test.ts index 0ced296a88..38ead236bd 100644 --- a/packages/techdocs-common/src/stages/publish/local.test.ts +++ b/packages/techdocs-common/src/stages/publish/local.test.ts @@ -25,10 +25,10 @@ import mockFs from 'mock-fs'; import * as os from 'os'; import { LocalPublish } from './local'; -const createMockEntity = (annotations = {}) => { +const createMockEntity = (annotations = {}, lowerCase = false) => { return { apiVersion: 'version', - kind: 'TestKind', + kind: lowerCase ? 'testkind' : 'TestKind', metadata: { name: 'test-component-name', annotations: { @@ -65,11 +65,42 @@ describe('local publisher', () => { const publisher = new LocalPublish(mockConfig, logger, testDiscovery); const mockEntity = createMockEntity(); + const lowerMockEntity = createMockEntity(undefined, true); await publisher.publish({ entity: mockEntity, directory: tmpDir }); expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true); + // Lower/upper should be treated the same. + expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(true); + + mockFs.restore(); + }); + + it('should respect legacy casing', async () => { + mockFs({ + [tmpDir]: { + 'index.html': '', + }, + }); + + const mockConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + + const publisher = new LocalPublish(mockConfig, logger, testDiscovery); + const mockEntity = createMockEntity(); + const lowerMockEntity = createMockEntity(undefined, true); + + await publisher.publish({ entity: mockEntity, directory: tmpDir }); + + expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true); + + // Lower/upper should be treated differently. + expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(false); + mockFs.restore(); }); @@ -86,6 +117,13 @@ describe('local publisher', () => { [resolvedDir]: { 'unsafe.html': '', 'unsafe.svg': '', + default: { + testkind: { + testname: { + 'index.html': 'found it', + }, + }, + }, }, }); }); @@ -107,5 +145,38 @@ describe('local publisher', () => { 'content-type': 'text/plain; charset=utf-8', }); }); + + it('should redirect case-sensitive triplet path to lower-case', async () => { + const response = await request(app) + .get('/default/TestKind/TestName/index.html') + .expect('Location', '/default/testkind/testname/index.html'); + expect(response.status).toBe(301); + }); + + it('should resolve lower-case triplet path content eventually', async () => { + const response = await request(app) + .get('/default/TestKind/TestName/index.html') + .redirects(1); + expect(response.text).toEqual('found it'); + }); + + it('should not redirect when legacy case setting is used', async () => { + const legacyConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + const legacyPublisher = new LocalPublish( + legacyConfig, + logger, + testDiscovery, + ); + app = express().use(legacyPublisher.docsRouter()); + + const response = await request(app).get( + '/default/TestKind/TestName/index.html', + ); + expect(response.status).toBe(404); + }); }); }); diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index 24af34932f..c547b65406 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -58,6 +58,8 @@ try { * called "static" at the root of techdocs-backend plugin. */ export class LocalPublish implements PublisherBase { + private legacyPathCasing: boolean; + // TODO: Use a static fromConfig method to create a LocalPublish instance, similar to aws/gcs publishers. // Move the logic of setting staticDocsDir based on config over to fromConfig, // and set the value as a class parameter. @@ -70,6 +72,10 @@ export class LocalPublish implements PublisherBase { this.config = config; this.logger = logger; this.discovery = discovery; + this.legacyPathCasing = + config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) || false; } async getReadiness(): Promise { @@ -81,8 +87,7 @@ export class LocalPublish implements PublisherBase { publish({ entity, directory }: PublishRequest): Promise { const entityNamespace = entity.metadata.namespace ?? 'default'; - const publishDir = path.join( - staticDocsDir, + const publishDir = this.staticEntityPathJoin( entityNamespace, entity.kind, entity.metadata.name, @@ -119,8 +124,7 @@ export class LocalPublish implements PublisherBase { async fetchTechDocsMetadata( entityName: EntityName, ): Promise { - const metadataPath = path.join( - staticDocsDir, + const metadataPath = this.staticEntityPathJoin( entityName.namespace, entityName.kind, entityName.name, @@ -138,23 +142,61 @@ export class LocalPublish implements PublisherBase { } docsRouter(): express.Handler { - return express.static(staticDocsDir, { - // Handle content-type header the same as all other publishers. - setHeaders: (res, filePath) => { - const fileExtension = path.extname(filePath); - const headers = getHeadersForFileExtension(fileExtension); - for (const [header, value] of Object.entries(headers)) { - res.setHeader(header, value); - } - }, + const router = express.Router(); + + // Redirect middleware ensuring that requests to case-sensitive entity + // triplet paths are always sent to lower-case versions. + router.use((req, res, next) => { + // If legacy path casing is on, let the request immediately continue. + if (this.legacyPathCasing) { + return next(); + } + + // Generate a lower-case entity triplet path. + const [_, namespace, kind, name, ...rest] = req.path.split('/'); + + // Ignore non-triplet objects. + if (!namespace || !kind || !name) { + return next(); + } + + const newPath = [ + _, + namespace.toLowerCase(), + kind.toLowerCase(), + name.toLowerCase(), + ...rest, + ].join('/'); + + // If there was no change, then let express.static() handle the request. + if (newPath === req.path) { + return next(); + } + + // Otherwise, redirect to the new path. + return res.redirect(req.baseUrl + newPath, 301); }); + + router.use( + express.static(staticDocsDir, { + // Handle content-type header the same as all other publishers. + setHeaders: (res, filePath) => { + const fileExtension = path.extname(filePath); + const headers = getHeadersForFileExtension(fileExtension); + for (const [header, value] of Object.entries(headers)) { + res.setHeader(header, value); + } + }, + }), + ); + + return router; } async hasDocsBeenGenerated(entity: Entity): Promise { const namespace = entity.metadata.namespace ?? 'default'; - const indexHtmlPath = path.join( - staticDocsDir, + const indexHtmlPath = this.staticEntityPathJoin( namespace, entity.kind, entity.metadata.name, @@ -210,4 +252,22 @@ export class LocalPublish implements PublisherBase { ), ); } + + /** + * Utility wrapper around path.join(), used to control legacy case logic. + */ + protected staticEntityPathJoin(...allParts: string[]): string { + if (this.legacyPathCasing) { + const [namespace, kind, name, ...parts] = allParts; + return path.join(staticDocsDir, namespace, kind, name, ...parts); + } + const [namespace, kind, name, ...parts] = allParts; + return path.join( + staticDocsDir, + namespace.toLowerCase(), + kind.toLowerCase(), + name.toLowerCase(), + ...parts, + ); + } } diff --git a/packages/techdocs-common/src/techdocsTypes.ts b/packages/techdocs-common/src/techdocsTypes.ts new file mode 100644 index 0000000000..4aa3972b31 --- /dev/null +++ b/packages/techdocs-common/src/techdocsTypes.ts @@ -0,0 +1,26 @@ +/* + * 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 { IndexableDocument } from '@backstage/search-common'; + +export interface TechDocsDocument extends IndexableDocument { + kind: string; + namespace: string; + name: string; + lifecycle: string; + owner: string; + path: string; +} diff --git a/plugins/allure/.eslintrc.js b/plugins/allure/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/allure/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md new file mode 100644 index 0000000000..d62b95ea66 --- /dev/null +++ b/plugins/allure/CHANGELOG.md @@ -0,0 +1,10 @@ +# @backstage/plugin-allure + +## 0.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.4.5 + - @backstage/core-components@0.4.0 + - @backstage/catalog-model@0.9.1 diff --git a/plugins/allure/README.md b/plugins/allure/README.md new file mode 100644 index 0000000000..b9a8435a34 --- /dev/null +++ b/plugins/allure/README.md @@ -0,0 +1,47 @@ +# [Allure](https://docs.qameta.io/allure/) + +Welcome to the Backstage Allure plugin. This plugin add an entity service page to display Allure test reports related to the service. + +## Install + +Run the below command from the `app` package directory. + +```shell +yarn add @backstage/plugin-allure +``` + +alternatively, you can execute below command from the root directory of your Backstage app. + +```shell +yarn workspace app add @backstage/plugin-allure +``` + +## Configure + +### Configure Allure service + +Add below configuration in the `app-config.yaml`. + +```yaml +allure: + baseUrl: # Example: https://allure.my-company.net or when running allure locally, http://localhost:5050/allure-docker-service +``` + +### Setup entity service page + +Add `EntityAllureReportContent` in the `EntityPage.tsx` like below: + +```diff ++ import { EntityAllureReportContent } from '@backstage/plugin-allure'; + +... + +const serviceEntityPage = ( + + ... ++ ++ ++ + +); +``` diff --git a/plugins/allure/api-report.md b/plugins/allure/api-report.md new file mode 100644 index 0000000000..c39b092954 --- /dev/null +++ b/plugins/allure/api-report.md @@ -0,0 +1,27 @@ +## API Report File for "@backstage/plugin-allure" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "allurePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const allurePlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// Warning: (ae-missing-release-tag) "EntityAllureReportContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityAllureReportContent: () => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/allure/dev/example-entity.yaml b/plugins/allure/dev/example-entity.yaml new file mode 100644 index 0000000000..58a9ffd110 --- /dev/null +++ b/plugins/allure/dev/example-entity.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: allure-example-service + description: Allure Example Service + annotations: + qameta.io/allure-project: sample +spec: + type: service + lifecycle: experimental + owner: team-a diff --git a/plugins/allure/dev/index.tsx b/plugins/allure/dev/index.tsx new file mode 100644 index 0000000000..d697febd56 --- /dev/null +++ b/plugins/allure/dev/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { EntityAllureReportContent } from '../src/plugin'; +import { Content, Header, Page } from '@backstage/core-components'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; + +import exampleEntity from './example-entity.yaml'; +import { allureApiRef } from '../src/api'; + +createDevApp() + .registerApi({ + api: allureApiRef, + deps: {}, + factory: () => + ({ + async getReportUrl(projectId: string) { + return Promise.resolve( + // Follow the instructions from https://github.com/fescobar/allure-docker-service-ui + // to setup Allure service locally + `http://localhost:5050/allure-docker-service/projects/${projectId}/reports/latest/index.html`, + ); + }, + } as unknown as typeof allureApiRef.T), + }) + .addPage({ + element: ( + +

+ + + + + + + ), + title: 'Allure Report', + path: '/allure', + }) + .render(); diff --git a/plugins/allure/package.json b/plugins/allure/package.json new file mode 100644 index 0000000000..efa44cb6f8 --- /dev/null +++ b/plugins/allure/package.json @@ -0,0 +1,69 @@ +{ + "name": "@backstage/plugin-allure", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.1", + "@backstage/core-components": "^0.4.1", + "@backstage/core-plugin-api": "^0.1.7", + "@backstage/plugin-catalog-react": "^0.4.5", + "@backstage/theme": "^0.2.10", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "^0.7.11", + "@backstage/core-app-api": "^0.1.12", + "@backstage/dev-utils": "^0.2.9", + "@backstage/test-utils": "^0.1.17", + "@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.29.0" + }, + "files": [ + "dist" + ], + "configSchema": { + "$schema": "https://backstage.io/schema/config-v1", + "title": "@backstage/allure", + "type": "object", + "properties": { + "allure": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "visibility": "frontend" + } + } + } + } + } +} diff --git a/plugins/allure/src/api/AllureApi.ts b/plugins/allure/src/api/AllureApi.ts new file mode 100644 index 0000000000..a926da03d0 --- /dev/null +++ b/plugins/allure/src/api/AllureApi.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. + */ +import { createApiRef } from '@backstage/core-plugin-api'; + +export type AllureApi = { + getReportUrl(projectId: string): Promise; +}; + +export const allureApiRef = createApiRef({ + id: 'allure-api', +}); diff --git a/plugins/allure/src/api/AllureApiClient.ts b/plugins/allure/src/api/AllureApiClient.ts new file mode 100644 index 0000000000..698713c837 --- /dev/null +++ b/plugins/allure/src/api/AllureApiClient.ts @@ -0,0 +1,30 @@ +/* + * 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 { ConfigApi } from '@backstage/core-plugin-api'; +import { AllureApi } from './AllureApi'; + +export class AllureApiClient implements AllureApi { + readonly configApi: ConfigApi; + + constructor(options: { configApi: ConfigApi }) { + this.configApi = options.configApi; + } + + async getReportUrl(projectId: string): Promise { + const baseUrl = this.configApi.getString('allure.baseUrl'); + return `${baseUrl}/projects/${projectId}/reports/latest/index.html`; + } +} diff --git a/plugins/allure/src/api/index.ts b/plugins/allure/src/api/index.ts new file mode 100644 index 0000000000..1c1adb1db2 --- /dev/null +++ b/plugins/allure/src/api/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 { allureApiRef } from './AllureApi'; +export { AllureApiClient } from './AllureApiClient'; diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx new file mode 100644 index 0000000000..19c7796e55 --- /dev/null +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 { AllureReportComponent } from './AllureReportComponent'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { msw, renderInTestApp } from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + msw.setupDefaultHandlers(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Missing Annotation')).toBeInTheDocument(); + }); +}); diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx new file mode 100644 index 0000000000..2ca3e87691 --- /dev/null +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.tsx @@ -0,0 +1,65 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import { allureApiRef } from '../../api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + ALLURE_PROJECT_ID_ANNOTATION, + isAllureReportAvailable, + getAllureProjectId, +} from '../annotationHelpers'; +import { + MissingAnnotationEmptyState, + Progress, +} from '@backstage/core-components'; +import { useAsync } from 'react-use'; +import { Entity } from '@backstage/catalog-model'; + +const AllureReport = (props: { entity: Entity }) => { + const allureApi = useApi(allureApiRef); + + const allureProjectId = getAllureProjectId(props.entity); + const { value, loading } = useAsync(async () => { + const url = await allureApi.getReportUrl(allureProjectId); + return url; + }); + + if (loading) { + return ; + } + return ( +