From 7bb1bde7f63354a6edd2448ec020e54d13d4c10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 3 Feb 2022 15:53:26 +0100 Subject: [PATCH 01/35] Bunch of random api cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/wise-peaches-flow.md | 8 + .../api-report.md | 10 +- .../src/microsoftGraph/index.ts | 3 +- plugins/catalog-graph/api-report.md | 4 - .../components/EntityRelationsGraph/types.ts | 7 +- plugins/catalog-import/api-report.md | 368 ++++++++++++------ .../src/api/CatalogImportApi.ts | 17 +- .../src/api/CatalogImportClient.ts | 49 ++- .../DefaultImportPage/DefaultImportPage.tsx | 5 + .../EntityListComponent.tsx | 32 +- .../components/EntityListComponent/index.ts | 1 + .../ImportInfoCard/ImportInfoCard.tsx | 24 +- .../src/components/ImportInfoCard/index.ts | 1 + .../src/components/ImportPage/ImportPage.tsx | 5 + .../ImportStepper/ImportStepper.tsx | 26 +- .../src/components/ImportStepper/defaults.tsx | 10 +- .../src/components/ImportStepper/index.ts | 1 + .../StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx | 24 +- .../components/StepInitAnalyzeUrl/index.ts | 1 + .../AutocompleteTextField.tsx | 42 +- .../PreparePullRequestForm.tsx | 23 +- .../PreviewCatalogInfoComponent.tsx | 23 +- .../PreviewPullRequestComponent.tsx | 23 +- .../StepPrepareCreatePullRequest.tsx | 25 +- .../StepPrepareCreatePullRequest/index.ts | 5 + .../catalog-import/src/components/index.ts | 1 + .../src/components/useImportState.ts | 12 +- plugins/catalog-import/src/plugin.ts | 11 + plugins/catalog-react/api-report.md | 124 +++--- .../EntityKindPicker/EntityKindPicker.tsx | 17 +- .../src/components/EntityKindPicker/index.ts | 1 + .../EntityLifecyclePicker.tsx | 1 + .../EntityOwnerPicker/EntityOwnerPicker.tsx | 1 + .../EntityRefLink/EntityRefLink.tsx | 11 + .../EntityRefLink/EntityRefLinks.tsx | 11 + .../src/components/EntityRefLink/index.ts | 3 + .../EntitySearchBar/EntitySearchBar.tsx | 1 + .../components/EntityTable/EntityTable.tsx | 31 +- .../src/components/EntityTable/columns.tsx | 15 +- .../src/components/EntityTable/index.ts | 2 + .../EntityTagPicker/EntityTagPicker.tsx | 1 + .../EntityTypePicker/EntityTypePicker.tsx | 12 +- .../src/components/EntityTypePicker/index.ts | 2 +- plugins/catalog-react/src/components/index.ts | 1 + scripts/api-extractor.ts | 2 + 45 files changed, 670 insertions(+), 327 deletions(-) create mode 100644 .changeset/wise-peaches-flow.md diff --git a/.changeset/wise-peaches-flow.md b/.changeset/wise-peaches-flow.md new file mode 100644 index 0000000000..68e89cf841 --- /dev/null +++ b/.changeset/wise-peaches-flow.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +--- + +Minor API cleanups diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 662056eff1..c00930d2ce 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -33,6 +33,15 @@ export function defaultUserTransformer( userPhoto?: string, ): Promise; +// @public +export type GroupMember = + | (MicrosoftGraph.Group & { + '@odata.type': '#microsoft.graph.user'; + }) + | (MicrosoftGraph.User & { + '@odata.type': '#microsoft.graph.group'; + }); + // @public export type GroupTransformer = ( group: MicrosoftGraph.Group, @@ -54,7 +63,6 @@ export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id'; export class MicrosoftGraphClient { constructor(baseUrl: string, pca: msal.ConfidentialClientApplication); static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient; - // Warning: (ae-forgotten-export) The symbol "GroupMember" needs to be exported by the entry point index.d.ts getGroupMembers(groupId: string): AsyncIterable; // (undocumented) getGroupPhoto(groupId: string, sizeId?: string): Promise; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts index b0d53b43a2..61e62f5803 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { MicrosoftGraphClient } from './client'; -export type { ODataQuery } from './client'; +export type { GroupMember, ODataQuery } from './client'; export { readMicrosoftGraphConfig } from './config'; export type { MicrosoftGraphProviderConfig } from './config'; export { diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index e39982d2b5..515a578d3c 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -95,8 +95,6 @@ export const EntityCatalogGraphCard: ({ // @public export type EntityEdge = DependencyGraphTypes.DependencyEdge; -// Warning: (ae-missing-release-tag) "EntityEdgeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityEdgeData = { relations: string[]; @@ -106,8 +104,6 @@ export type EntityEdgeData = { // @public export type EntityNode = DependencyGraphTypes.DependencyNode; -// Warning: (ae-missing-release-tag) "EntityNodeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityNodeData = { name: string; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts index 830032d527..caf6c8c501 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DependencyGraphTypes } from '@backstage/core-components'; import { MouseEventHandler } from 'react'; /** - * Additional Data for entities + * Additional Data for entities. + * + * @public */ export type EntityEdgeData = { /** @@ -40,6 +43,8 @@ export type EntityEdge = DependencyGraphTypes.DependencyEdge; /** * Additional data for Entity Node + * + * @public */ export type EntityNodeData = { /** diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index a85777c563..164d831c96 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -26,9 +26,7 @@ import { UnpackNestedValue } from 'react-hook-form'; import { UseFormProps } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form'; -// Warning: (ae-missing-release-tag) "AnalyzeResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AnalyzeResult = | { type: 'locations'; @@ -45,26 +43,36 @@ export type AnalyzeResult = generatedEntities: PartialEntity[]; }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "AutocompleteTextField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const AutocompleteTextField: ({ - name, - options, - required, - errors, - rules, - loading, - loadingText, - helperText, - errorHelperText, - textFieldProps, -}: Props_5) => JSX.Element; +// @public +export const AutocompleteTextField: ( + props: AutocompleteTextFieldProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "CatalogImportApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export interface AutocompleteTextFieldProps { + // (undocumented) + errorHelperText?: string; + // (undocumented) + errors?: FieldErrors; + // (undocumented) + helperText?: React_2.ReactNode; + // (undocumented) + loading?: boolean; + // (undocumented) + loadingText?: string; + // (undocumented) + name: TFieldValue; + // (undocumented) + options: string[]; + // (undocumented) + required?: boolean; + // (undocumented) + rules?: React_2.ComponentProps['rules']; + // (undocumented) + textFieldProps?: Omit; +} + +// @public export interface CatalogImportApi { // (undocumented) analyzeUrl(url: string): Promise; @@ -85,14 +93,10 @@ export interface CatalogImportApi { }>; } -// Warning: (ae-missing-release-tag) "catalogImportApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const catalogImportApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "CatalogImportClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class CatalogImportClient implements CatalogImportApi { constructor(options: { discoveryApi: DiscoveryApi; @@ -110,12 +114,7 @@ export class CatalogImportClient implements CatalogImportApi { body: string; }>; // (undocumented) - submitPullRequest({ - repositoryUrl, - fileContent, - title, - body, - }: { + submitPullRequest(options: { repositoryUrl: string; fileContent: string; title: string; @@ -126,14 +125,10 @@ export class CatalogImportClient implements CatalogImportApi { }>; } -// Warning: (ae-missing-release-tag) "CatalogImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const CatalogImportPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "catalogImportPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public const catalogImportPlugin: BackstagePlugin< { importPage: RouteRef; @@ -143,9 +138,7 @@ const catalogImportPlugin: BackstagePlugin< export { catalogImportPlugin }; export { catalogImportPlugin as plugin }; -// Warning: (ae-forgotten-export) The symbol "ImportFlows" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "StepperProvider" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "defaultGenerateStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function defaultGenerateStepper( @@ -153,98 +146,221 @@ export function defaultGenerateStepper( defaults: StepperProvider, ): StepperProvider; -// Warning: (ae-missing-release-tag) "DefaultImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const DefaultImportPage: () => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityListComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityListComponent: ({ - locations, - collapsed, - locationListItemIcon, - onItemClick, - firstListItem, - withLinks, -}: Props) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ImportInfoCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const ImportInfoCard: ({ - exampleLocationUrl, - exampleRepositoryUrl, -}: Props_2) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ImportStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const ImportStepper: ({ - initialUrl, - generateStepper, - variant, -}: Props_3) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "PreparePullRequestForm" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const PreparePullRequestForm: < +export const EntityListComponent: ( + props: EntityListComponentProps, +) => JSX.Element; + +// @public +export interface EntityListComponentProps { + // (undocumented) + collapsed?: boolean; + // (undocumented) + firstListItem?: React_2.ReactElement; + // (undocumented) + locationListItemIcon: (target: string) => React_2.ReactElement; + // (undocumented) + locations: Array<{ + target: string; + entities: (Entity | EntityName)[]; + }>; + // (undocumented) + onItemClick?: (target: string) => void; + // (undocumented) + withLinks?: boolean; +} + +// @public +export type ImportFlows = + | 'unknown' + | 'single-location' + | 'multiple-locations' + | 'no-location'; + +// @public +export const ImportInfoCard: (props: ImportInfoCardProps) => JSX.Element; + +// @public +export interface ImportInfoCardProps { + // (undocumented) + exampleLocationUrl?: string; + // (undocumented) + exampleRepositoryUrl?: string; +} + +// Warning: (ae-forgotten-export) The symbol "State" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ImportState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ImportState = State & { + activeFlow: ImportFlows; + activeStepNumber: number; + analysisUrl?: string; + onGoBack?: () => void; + onReset: () => void; +}; + +// @public +export const ImportStepper: (props: ImportStepperProps) => JSX.Element; + +// @public +export interface ImportStepperProps { + // (undocumented) + generateStepper?: ( + flow: ImportFlows, + defaults: StepperProvider, + ) => StepperProvider; + // (undocumented) + initialUrl?: string; + // (undocumented) + variant?: InfoCardVariants; +} + +// @public +export const PreparePullRequestForm: >( + props: PreparePullRequestFormProps, +) => JSX.Element; + +// @public +export type PreparePullRequestFormProps< TFieldValues extends Record, ->({ - defaultValues, - onSubmit, - render, -}: Props_6) => JSX.Element; +> = Pick, 'defaultValues'> & { + onSubmit: SubmitHandler; + render: ( + props: Pick< + UseFormReturn, + 'formState' | 'register' | 'control' | 'setValue' + > & { + values: UnpackNestedValue; + }, + ) => React_2.ReactNode; +}; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "PreviewCatalogInfoComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const PreviewCatalogInfoComponent: ({ - repositoryUrl, - entities, - classes, -}: Props_7) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "PreviewPullRequestComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const PreviewPullRequestComponent: ({ - title, - description, - classes, -}: Props_8) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "StepInitAnalyzeUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const StepInitAnalyzeUrl: ({ - onAnalysis, - analysisUrl, - disablePullRequest, - exampleLocationUrl, -}: Props_4) => JSX.Element; +export type PrepareResult = + | { + type: 'locations'; + locations: Array<{ + exists?: boolean; + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "StepPrepareCreatePullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const StepPrepareCreatePullRequest: ({ - analyzeResult, - onPrepare, - onGoBack, - renderFormFields, -}: Props_9) => JSX.Element; +// @public +export const PreviewCatalogInfoComponent: ( + props: PreviewCatalogInfoComponentProps, +) => JSX.Element; + +// @public +export interface PreviewCatalogInfoComponentProps { + // (undocumented) + classes?: { + card?: string; + cardContent?: string; + }; + // (undocumented) + entities: Entity[]; + // (undocumented) + repositoryUrl: string; +} + +// @public +export const PreviewPullRequestComponent: ( + props: PreviewPullRequestComponentProps, +) => JSX.Element; + +// @public +export interface PreviewPullRequestComponentProps { + // (undocumented) + classes?: { + card?: string; + cardContent?: string; + }; + // (undocumented) + description: string; + // (undocumented) + title: string; +} + +// @public +export const StepInitAnalyzeUrl: ( + props: StepInitAnalyzeUrlProps, +) => JSX.Element; + +// @public +export interface StepInitAnalyzeUrlProps { + // (undocumented) + analysisUrl?: string; + // (undocumented) + disablePullRequest?: boolean; + // (undocumented) + exampleLocationUrl?: string; + // (undocumented) + onAnalysis: ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { + prepareResult?: PrepareResult; + }, + ) => void; +} + +// @public +export const StepPrepareCreatePullRequest: ( + props: StepPrepareCreatePullRequestProps, +) => JSX.Element; + +// @public +export interface StepPrepareCreatePullRequestProps { + // (undocumented) + analyzeResult: Extract< + AnalyzeResult, + { + type: 'repository'; + } + >; + // (undocumented) + onGoBack?: () => void; + // (undocumented) + onPrepare: ( + result: PrepareResult, + opts?: { + notRepeatable?: boolean; + }, + ) => void; + // Warning: (ae-forgotten-export) The symbol "FormData" needs to be exported by the entry point index.d.ts + // + // (undocumented) + renderFormFields: ( + props: Pick< + UseFormReturn, + 'register' | 'setValue' | 'formState' + > & { + values: UnpackNestedValue; + groups: string[]; + groupsLoading: boolean; + }, + ) => React_2.ReactNode; +} // Warnings were encountered during analysis: // -// src/api/CatalogImportApi.d.ts:15:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts +// src/api/CatalogImportApi.d.ts:25:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 5fae076479..0923f4ac98 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -18,11 +18,20 @@ import { EntityName } from '@backstage/catalog-model'; import { createApiRef } from '@backstage/core-plugin-api'; import { PartialEntity } from '../types'; +/** + * Utility API reference for the {@link CatalogImportApi}. + * + * @public + */ export const catalogImportApiRef = createApiRef({ id: 'plugin.catalog-import.service', }); -// result of the analyze state +/** + * Result of the analysis. + * + * @public + */ export type AnalyzeResult = | { type: 'locations'; @@ -39,6 +48,11 @@ export type AnalyzeResult = generatedEntities: PartialEntity[]; }; +/** + * API for driving catalog imports. + * + * @public + */ export interface CatalogImportApi { analyzeUrl(url: string): Promise; @@ -46,6 +60,7 @@ export interface CatalogImportApi { title: string; body: string; }>; + submitPullRequest(options: { repositoryUrl: string; fileContent: string; diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 60745923b4..b856a4e359 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -34,6 +34,11 @@ import { getGithubIntegrationConfig } from './GitHub'; import { trimEnd } from 'lodash'; import { getBranchName, getCatalogFilename } from '../components/helpers'; +/** + * The default implementation of the {@link CatalogImportApi}. + * + * @public + */ export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; @@ -142,17 +147,14 @@ the component will become available.\n\nFor more information, read an \ }; } - async submitPullRequest({ - repositoryUrl, - fileContent, - title, - body, - }: { + async submitPullRequest(options: { repositoryUrl: string; fileContent: string; title: string; body: string; }): Promise<{ link: string; location: string }> { + const { repositoryUrl, fileContent, title, body } = options; + const ghConfig = getGithubIntegrationConfig( this.scmIntegrationsApi, repositoryUrl, @@ -172,9 +174,7 @@ the component will become available.\n\nFor more information, read an \ } // TODO: this could be part of the catalog api - private async generateEntityDefinitions({ - repo, - }: { + private async generateEntityDefinitions(options: { repo: string; }): Promise { const { token } = await this.identityApi.getCredentials(); @@ -187,7 +187,7 @@ the component will become available.\n\nFor more information, read an \ }, method: 'POST', body: JSON.stringify({ - location: { type: 'url', target: repo }, + location: { type: 'url', target: options.repo }, }), }, ).catch(e => { @@ -204,12 +204,7 @@ the component will become available.\n\nFor more information, read an \ } // TODO: this response should better be part of the analyze-locations response and scm-independent / implemented per scm - private async checkGitHubForExistingCatalogInfo({ - url, - owner, - repo, - githubIntegrationConfig, - }: { + private async checkGitHubForExistingCatalogInfo(options: { url: string; owner: string; repo: string; @@ -220,6 +215,8 @@ the component will become available.\n\nFor more information, read an \ entities: EntityName[]; }> > { + const { url, owner, repo, githubIntegrationConfig } = options; + const { token } = await this.scmAuthApi.getCredentials({ url }); const octo = new Octokit({ auth: token, @@ -269,15 +266,7 @@ the component will become available.\n\nFor more information, read an \ } // TODO: extract this function and implement for non-github - private async submitGitHubPrToRepo({ - owner, - repo, - title, - body, - fileContent, - repositoryUrl, - githubIntegrationConfig, - }: { + private async submitGitHubPrToRepo(options: { owner: string; repo: string; title: string; @@ -286,6 +275,16 @@ the component will become available.\n\nFor more information, read an \ repositoryUrl: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }> { + const { + owner, + repo, + title, + body, + fileContent, + repositoryUrl, + githubIntegrationConfig, + } = options; + const { token } = await this.scmAuthApi.getCredentials({ url: repositoryUrl, additionalScope: { diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index 8693895ec0..30cce9603a 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -27,6 +27,11 @@ import React from 'react'; import { ImportInfoCard } from '../ImportInfoCard'; import { ImportStepper } from '../ImportStepper'; +/** + * The default catalog import page. + * + * @public + */ export const DefaultImportPage = () => { const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx index b8af9942f6..2c59eec18f 100644 --- a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -47,23 +47,35 @@ function sortEntities(entities: Array) { ); } -type Props = { +/** + * Props for {@link EntityListComponent}. + * + * @public + */ +export interface EntityListComponentProps { locations: Array<{ target: string; entities: (Entity | EntityName)[] }>; locationListItemIcon: (target: string) => React.ReactElement; collapsed?: boolean; firstListItem?: React.ReactElement; onItemClick?: (target: string) => void; withLinks?: boolean; -}; +} + +/** + * Shows a result list of entities. + * + * @public + */ +export const EntityListComponent = (props: EntityListComponentProps) => { + const { + locations, + collapsed = false, + locationListItemIcon, + onItemClick, + firstListItem, + withLinks = false, + } = props; -export const EntityListComponent = ({ - locations, - collapsed = false, - locationListItemIcon, - onItemClick, - firstListItem, - withLinks = false, -}: Props) => { const app = useApp(); const classes = useStyles(); diff --git a/plugins/catalog-import/src/components/EntityListComponent/index.ts b/plugins/catalog-import/src/components/EntityListComponent/index.ts index 06b695240c..7fe0a0d55c 100644 --- a/plugins/catalog-import/src/components/EntityListComponent/index.ts +++ b/plugins/catalog-import/src/components/EntityListComponent/index.ts @@ -15,3 +15,4 @@ */ export { EntityListComponent } from './EntityListComponent'; +export type { EntityListComponentProps } from './EntityListComponent'; diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx index 3e66e137eb..c879cbe998 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx @@ -21,15 +21,27 @@ import React from 'react'; import { catalogImportApiRef } from '../../api'; import { useCatalogFilename } from '../../hooks'; -type Props = { +/** + * Props for {@link ImportInfoCard}. + * + * @public + */ +export interface ImportInfoCardProps { exampleLocationUrl?: string; exampleRepositoryUrl?: string; -}; +} + +/** + * Shows information about the import process. + * + * @public + */ +export const ImportInfoCard = (props: ImportInfoCardProps) => { + const { + exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + exampleRepositoryUrl = 'https://github.com/backstage/backstage', + } = props; -export const ImportInfoCard = ({ - exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', - exampleRepositoryUrl = 'https://github.com/backstage/backstage', -}: Props) => { const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; const catalogImportApi = useApi(catalogImportApiRef); diff --git a/plugins/catalog-import/src/components/ImportInfoCard/index.ts b/plugins/catalog-import/src/components/ImportInfoCard/index.ts index c82e88f7e8..10dc8726f6 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/index.ts +++ b/plugins/catalog-import/src/components/ImportInfoCard/index.ts @@ -15,3 +15,4 @@ */ export { ImportInfoCard } from './ImportInfoCard'; +export type { ImportInfoCardProps } from './ImportInfoCard'; diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx index 467bd710f0..129e714952 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx @@ -18,6 +18,11 @@ import React from 'react'; import { useOutlet } from 'react-router'; import { DefaultImportPage } from '../DefaultImportPage'; +/** + * The whole catalog import page. + * + * @public + */ export const ImportPage = () => { const outlet = useOutlet(); diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx index 4112775c2d..b2638e582e 100644 --- a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx @@ -34,20 +34,32 @@ const useStyles = makeStyles(() => ({ }, })); -type Props = { +/** + * Props for {@link ImportStepper}. + * + * @public + */ +export interface ImportStepperProps { initialUrl?: string; generateStepper?: ( flow: ImportFlows, defaults: StepperProvider, ) => StepperProvider; variant?: InfoCardVariants; -}; +} + +/** + * The stepper that holds the different import stages. + * + * @public + */ +export const ImportStepper = (props: ImportStepperProps) => { + const { + initialUrl, + generateStepper = defaultGenerateStepper, + variant, + } = props; -export const ImportStepper = ({ - initialUrl, - generateStepper = defaultGenerateStepper, - variant, -}: Props) => { const catalogImportApi = useApi(catalogImportApiRef); const classes = useStyles(); const state = useImportState({ initialUrl }); diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx index 553e8977b8..94dff39d0f 100644 --- a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx @@ -42,7 +42,12 @@ export type StepConfiguration = { content: React.ReactElement; }; -export type StepperProvider = { +/** + * Defines the details of the stepper. + * + * @public + */ +export interface StepperProvider { analyze: ( s: Extract, opts: { apis: StepperApis }, @@ -59,7 +64,7 @@ export type StepperProvider = { s: Extract, opts: { apis: StepperApis }, ) => StepConfiguration; -}; +} /** * The default stepper generation function. @@ -69,6 +74,7 @@ export type StepperProvider = { * * @param flow - the name of the active flow * @param defaults - the default steps + * @public */ export function defaultGenerateStepper( flow: ImportFlows, diff --git a/plugins/catalog-import/src/components/ImportStepper/index.ts b/plugins/catalog-import/src/components/ImportStepper/index.ts index 164c6f43f1..db940f5f74 100644 --- a/plugins/catalog-import/src/components/ImportStepper/index.ts +++ b/plugins/catalog-import/src/components/ImportStepper/index.ts @@ -15,4 +15,5 @@ */ export { ImportStepper } from './ImportStepper'; +export type { ImportStepperProps } from './ImportStepper'; export { defaultGenerateStepper } from './defaults'; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx index 83377d6b2f..3c655ec11b 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -27,7 +27,12 @@ type FormData = { url: string; }; -type Props = { +/** + * Props for {@link StepInitAnalyzeUrl}. + * + * @public + */ +export interface StepInitAnalyzeUrlProps { onAnalysis: ( flow: ImportFlows, url: string, @@ -37,7 +42,7 @@ type Props = { disablePullRequest?: boolean; analysisUrl?: string; exampleLocationUrl?: string; -}; +} /** * A form that lets the user input a url and analyze it for existing locations or potential entities. @@ -45,13 +50,16 @@ type Props = { * @param onAnalysis - is called when the analysis was successful * @param analysisUrl - a url that can be used as a default value * @param disablePullRequest - if true, repositories without entities will abort the wizard + * @public */ -export const StepInitAnalyzeUrl = ({ - onAnalysis, - analysisUrl = '', - disablePullRequest = false, - exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', -}: Props) => { +export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { + const { + onAnalysis, + analysisUrl = '', + disablePullRequest = false, + exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + } = props; + const errorApi = useApi(errorApiRef); const catalogImportApi = useApi(catalogImportApiRef); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts index 2cd1557d75..2a3dffcf99 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts @@ -15,3 +15,4 @@ */ export { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; +export type { StepInitAnalyzeUrlProps } from './StepInitAnalyzeUrl'; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx index 98d741df15..2e163be2aa 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx @@ -20,7 +20,12 @@ import { Autocomplete } from '@material-ui/lab'; import React from 'react'; import { Controller, FieldErrors } from 'react-hook-form'; -type Props = { +/** + * Props for {@link AutocompleteTextField}. + * + * @public + */ +export interface AutocompleteTextFieldProps { name: TFieldValue; options: string[]; required?: boolean; @@ -35,20 +40,29 @@ type Props = { errorHelperText?: string; textFieldProps?: Omit; -}; +} + +/** + * An autocompletion text field for the catalog import flows. + * + * @public + */ +export const AutocompleteTextField = ( + props: AutocompleteTextFieldProps, +) => { + const { + name, + options, + required, + errors, + rules, + loading = false, + loadingText, + helperText, + errorHelperText, + textFieldProps = {}, + } = props; -export const AutocompleteTextField = ({ - name, - options, - required, - errors, - rules, - loading = false, - loadingText, - helperText, - errorHelperText, - textFieldProps = {}, -}: Props) => { return ( > = Pick< - UseFormProps, - 'defaultValues' -> & { +/** + * Props for {@link PreparePullRequestForm}. + * + * @public + */ +export type PreparePullRequestFormProps< + TFieldValues extends Record, +> = Pick, 'defaultValues'> & { onSubmit: SubmitHandler; render: ( @@ -48,14 +52,15 @@ type Props> = Pick< * @param onSubmit - a callback that is executed when the form is submitted * (initiated by a button of type="submit") * @param render - render the form elements + * @public */ export const PreparePullRequestForm = < TFieldValues extends Record, ->({ - defaultValues, - onSubmit, - render, -}: Props) => { +>( + props: PreparePullRequestFormProps, +) => { + const { defaultValues, onSubmit, render } = props; + const methods = useForm({ mode: 'onTouched', defaultValues }); const { handleSubmit, watch, control, register, formState, setValue } = methods; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx index 0f1dc33d2e..a16276f90b 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx @@ -22,17 +22,26 @@ import { CodeSnippet } from '@backstage/core-components'; import { trimEnd } from 'lodash'; import { useCatalogFilename } from '../../hooks'; -type Props = { +/** + * Props for {@link PreviewCatalogInfoComponent}. + * + * @public + */ +export interface PreviewCatalogInfoComponentProps { repositoryUrl: string; entities: Entity[]; classes?: { card?: string; cardContent?: string }; -}; +} -export const PreviewCatalogInfoComponent = ({ - repositoryUrl, - entities, - classes, -}: Props) => { +/** + * Previews information about an entity to create. + * + * @public + */ +export const PreviewCatalogInfoComponent = ( + props: PreviewCatalogInfoComponentProps, +) => { + const { repositoryUrl, entities, classes } = props; const catalogFilename = useCatalogFilename(); return ( diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx index 7b6653e4ca..e8a54489a0 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx @@ -18,17 +18,26 @@ import { Card, CardContent, CardHeader } from '@material-ui/core'; import React from 'react'; import { MarkdownContent } from '@backstage/core-components'; -type Props = { +/** + * Props for {@link PreviewPullRequestComponent}. + * + * @public + */ +export interface PreviewPullRequestComponentProps { title: string; description: string; classes?: { card?: string; cardContent?: string }; -}; +} -export const PreviewPullRequestComponent = ({ - title, - description, - classes, -}: Props) => { +/** + * Previews a pull request. + * + * @public + */ +export const PreviewPullRequestComponent = ( + props: PreviewPullRequestComponentProps, +) => { + const { title, description, classes } = props; return ( diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index a79caaa0b9..36e8c33a5c 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -53,7 +53,12 @@ type FormData = { useCodeowners: boolean; }; -type Props = { +/** + * Props for {@link StepPrepareCreatePullRequest}. + * + * @public + */ +export interface StepPrepareCreatePullRequestProps { analyzeResult: Extract; onPrepare: ( result: PrepareResult, @@ -71,7 +76,7 @@ type Props = { groupsLoading: boolean; }, ) => React.ReactNode; -}; +} export function generateEntities( entities: PartialEntity[], @@ -93,12 +98,16 @@ export function generateEntities( })); } -export const StepPrepareCreatePullRequest = ({ - analyzeResult, - onPrepare, - onGoBack, - renderFormFields, -}: Props) => { +/** + * Prepares a pull request. + * + * @public + */ +export const StepPrepareCreatePullRequest = ( + props: StepPrepareCreatePullRequestProps, +) => { + const { analyzeResult, onPrepare, onGoBack, renderFormFields } = props; + const classes = useStyles(); const catalogApi = useApi(catalogApiRef); const catalogImportApi = useApi(catalogImportApiRef); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts index 1e8eaea1b9..ce1cd40e19 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts @@ -15,7 +15,12 @@ */ export { AutocompleteTextField } from './AutocompleteTextField'; +export type { AutocompleteTextFieldProps } from './AutocompleteTextField'; export { PreparePullRequestForm } from './PreparePullRequestForm'; +export type { PreparePullRequestFormProps } from './PreparePullRequestForm'; export { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +export type { PreviewCatalogInfoComponentProps } from './PreviewCatalogInfoComponent'; export { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; +export type { PreviewPullRequestComponentProps } from './PreviewPullRequestComponent'; export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; +export type { StepPrepareCreatePullRequestProps } from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/index.ts b/plugins/catalog-import/src/components/index.ts index 886c679d2d..15f57e7219 100644 --- a/plugins/catalog-import/src/components/index.ts +++ b/plugins/catalog-import/src/components/index.ts @@ -20,3 +20,4 @@ export * from './ImportInfoCard'; export * from './ImportStepper'; export * from './StepInitAnalyzeUrl'; export * from './StepPrepareCreatePullRequest'; +export type { ImportFlows, ImportState, PrepareResult } from './useImportState'; diff --git a/plugins/catalog-import/src/components/useImportState.ts b/plugins/catalog-import/src/components/useImportState.ts index a1505b26b5..0cb6b59917 100644 --- a/plugins/catalog-import/src/components/useImportState.ts +++ b/plugins/catalog-import/src/components/useImportState.ts @@ -18,7 +18,11 @@ import { Entity, EntityName } from '@backstage/catalog-model'; import { useReducer } from 'react'; import { AnalyzeResult } from '../api'; -// the configuration of the stepper +/** + * The configuration of the stepper. + * + * @public + */ export type ImportFlows = | 'unknown' | 'single-location' @@ -28,7 +32,11 @@ export type ImportFlows = // the available states of the stepper type ImportStateTypes = 'analyze' | 'prepare' | 'review' | 'finish'; -// result of the prepare state +/** + * Result of the prepare state. + * + * @public + */ export type PrepareResult = | { type: 'locations'; diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts index 52c1929dce..d9dd226792 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -34,6 +34,12 @@ export const rootRouteRef = createRouteRef({ id: 'catalog-import', }); +/** + * A plugin that helps the user in importing projects and YAML files into the + * catalog. + * + * @public + */ export const catalogImportPlugin = createPlugin({ id: 'catalog-import', apis: [ @@ -70,6 +76,11 @@ export const catalogImportPlugin = createPlugin({ }, }); +/** + * The page for importing projects and YAML files into the catalog. + * + * @public + */ export const CatalogImportPage = catalogImportPlugin.provide( createRoutableExtension({ name: 'CatalogImportPage', diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 8c3ba0daa3..2c064a504f 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -105,22 +105,14 @@ export type CatalogReactUserListPickerClassKey = // @public @deprecated (undocumented) export const catalogRouteRef: RouteRef; -// Warning: (ae-missing-release-tag) "createDomainColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createDomainColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createEntityRefColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -function createEntityRefColumn({ - defaultKind, -}: { +function createEntityRefColumn(options: { defaultKind?: string; }): TableColumn; -// Warning: (ae-missing-release-tag) "createEntityRelationColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createEntityRelationColumn({ title, @@ -136,28 +128,18 @@ function createEntityRelationColumn({ }; }): TableColumn; -// Warning: (ae-missing-release-tag) "createMetadataDescriptionColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createMetadataDescriptionColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createOwnerColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createOwnerColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createSpecLifecycleColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createSpecLifecycleColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createSpecTypeColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createSpecTypeColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createSystemColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createSystemColumn(): TableColumn; @@ -215,14 +197,18 @@ export class EntityKindFilter implements EntityFilter { readonly value: string; } -// Warning: (ae-forgotten-export) The symbol "EntityKindFilterProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityKindPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityKindPicker: ({ - initialFilter, - hidden, -}: EntityKindFilterProps) => JSX.Element | null; +export const EntityKindPicker: ( + props: EntityKindPickerProps, +) => JSX.Element | null; + +// @public +export interface EntityKindPickerProps { + // (undocumented) + hidden: boolean; + // (undocumented) + initialFilter?: string; +} // Warning: (ae-missing-release-tag) "EntityLifecycleFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -237,8 +223,6 @@ export class EntityLifecycleFilter implements EntityFilter { readonly values: string[]; } -// Warning: (ae-missing-release-tag) "EntityLifecyclePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityLifecyclePicker: () => JSX.Element | null; @@ -270,8 +254,6 @@ export class EntityOwnerFilter implements EntityFilter { readonly values: string[]; } -// Warning: (ae-missing-release-tag) "EntityOwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityOwnerPicker: () => JSX.Element | null; @@ -289,10 +271,7 @@ export interface EntityProviderProps { entity?: Entity; } -// Warning: (ae-forgotten-export) The symbol "EntityRefLinkProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityRefLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const EntityRefLink: React_2.ForwardRefExoticComponent< Pick< EntityRefLinkProps, @@ -581,16 +560,27 @@ export const EntityRefLink: React_2.ForwardRefExoticComponent< React_2.RefAttributes >; -// Warning: (ae-forgotten-export) The symbol "EntityRefLinksProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityRefLinks" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type EntityRefLinkProps = { + entityRef: Entity | EntityName; + defaultKind?: string; + title?: string; + children?: React_2.ReactNode; +} & Omit; + +// @public export const EntityRefLinks: ({ entityRefs, defaultKind, ...linkProps }: EntityRefLinksProps) => JSX.Element; +// @public +export type EntityRefLinksProps = { + entityRefs: (Entity | EntityName)[]; + defaultKind?: string; +} & Omit; + // Warning: (ae-missing-release-tag) "entityRoute" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) @@ -618,8 +608,6 @@ export const entityRouteRef: RouteRef<{ namespace: string; }>; -// Warning: (ae-missing-release-tag) "EntitySearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntitySearchBar: () => JSX.Element; @@ -631,18 +619,12 @@ export type EntitySourceLocation = { integrationType?: string; }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "EntityTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export function EntityTable({ - entities, - title, - emptyContent, - variant, - columns, -}: Props): JSX.Element; +// @public +export function EntityTable( + props: EntityTableProps, +): JSX.Element; // @public (undocumented) export namespace EntityTable { @@ -656,6 +638,22 @@ export namespace EntityTable { componentEntityColumns: TableColumn[]; } +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "EntityTable" has more than one declaration; you need to add a TSDoc member reference selector +// +// @public +export interface EntityTableProps { + // (undocumented) + columns: TableColumn[]; + // (undocumented) + emptyContent?: ReactNode; + // (undocumented) + entities: T[]; + // (undocumented) + title: string; + // (undocumented) + variant?: 'gridItem'; +} + // Warning: (ae-missing-release-tag) "EntityTagFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -669,8 +667,6 @@ export class EntityTagFilter implements EntityFilter { readonly values: string[]; } -// Warning: (ae-missing-release-tag) "EntityTagPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityTagPicker: () => JSX.Element | null; @@ -700,26 +696,24 @@ export class EntityTypeFilter implements EntityFilter { readonly value: string | string[]; } -// Warning: (ae-missing-release-tag) "EntityTypeFilterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type EntityTypeFilterProps = { - initialFilter?: string; - hidden?: boolean; -}; - -// Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityTypePicker: ( - props: EntityTypeFilterProps, + props: EntityTypePickerProps, ) => JSX.Element | null; +// @public +export interface EntityTypePickerProps { + // (undocumented) + hidden?: boolean; + // (undocumented) + initialFilter?: string; +} + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "FavoriteEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export const FavoriteEntity: (props: Props_2) => JSX.Element; +export const FavoriteEntity: (props: Props) => JSX.Element; // Warning: (ae-missing-release-tag) "favoriteEntityIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -835,7 +829,7 @@ export const UnregisterEntityDialog: ({ onConfirm, onClose, entity, -}: Props_3) => JSX.Element; +}: Props_2) => JSX.Element; // @public export function useEntity(): { diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx index 6f9119c404..17d28aad9f 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx @@ -19,15 +19,20 @@ import { Alert } from '@material-ui/lab'; import { useEntityListProvider } from '../../hooks'; import { EntityKindFilter } from '../../filters'; -type EntityKindFilterProps = { +/** + * Props for {@link EntityKindPicker}. + * + * @public + */ +export interface EntityKindPickerProps { initialFilter?: string; hidden: boolean; -}; +} + +/** @public */ +export const EntityKindPicker = (props: EntityKindPickerProps) => { + const { initialFilter, hidden } = props; -export const EntityKindPicker = ({ - initialFilter, - hidden, -}: EntityKindFilterProps) => { const { updateFilters, queryParameters } = useEntityListProvider(); const [selectedKind] = useState( [queryParameters.kind].flat()[0] ?? initialFilter, diff --git a/plugins/catalog-react/src/components/EntityKindPicker/index.ts b/plugins/catalog-react/src/components/EntityKindPicker/index.ts index 89dd46230b..a89a96aebc 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/index.ts +++ b/plugins/catalog-react/src/components/EntityKindPicker/index.ts @@ -15,3 +15,4 @@ */ export { EntityKindPicker } from './EntityKindPicker'; +export type { EntityKindPickerProps } from './EntityKindPicker'; diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index 630b1700a0..f5f973b829 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -46,6 +46,7 @@ const useStyles = makeStyles( const icon = ; const checkedIcon = ; +/** @public */ export const EntityLifecyclePicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index ae88858666..e807155b0c 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -48,6 +48,7 @@ const useStyles = makeStyles( const icon = ; const checkedIcon = ; +/** @public */ export const EntityOwnerPicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 02e63e60e3..aa92fd0818 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, EntityName, @@ -25,6 +26,11 @@ import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Tooltip } from '@material-ui/core'; +/** + * Props for {@link EntityRefLink}. + * + * @public + */ export type EntityRefLinkProps = { entityRef: Entity | EntityName; defaultKind?: string; @@ -32,6 +38,11 @@ export type EntityRefLinkProps = { children?: React.ReactNode; } & Omit; +/** + * Shows a clickable link to an entity. + * + * @public + */ export const EntityRefLink = forwardRef( (props, ref) => { const { entityRef, defaultKind, title, children, ...linkProps } = props; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx index 4a990f4028..9be970aa56 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx @@ -13,16 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, EntityName } from '@backstage/catalog-model'; import React from 'react'; import { EntityRefLink } from './EntityRefLink'; import { LinkProps } from '@backstage/core-components'; +/** + * Props for {@link EntityRefLink}. + * + * @public + */ export type EntityRefLinksProps = { entityRefs: (Entity | EntityName)[]; defaultKind?: string; } & Omit; +/** + * Shows a list of clickable links to entities. + * + * @public + */ export const EntityRefLinks = ({ entityRefs, defaultKind, diff --git a/plugins/catalog-react/src/components/EntityRefLink/index.ts b/plugins/catalog-react/src/components/EntityRefLink/index.ts index 1c5297e2f2..d49993feb6 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/index.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/index.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { EntityRefLink } from './EntityRefLink'; +export type { EntityRefLinkProps } from './EntityRefLink'; export { EntityRefLinks } from './EntityRefLinks'; +export type { EntityRefLinksProps } from './EntityRefLinks'; export { formatEntityRefTitle } from './format'; diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx index 79b2ee36bf..ec977fada4 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx @@ -45,6 +45,7 @@ const useStyles = makeStyles( }, ); +/** @public */ export const EntitySearchBar = () => { const classes = useStyles(); diff --git a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx index 43da2b3612..62c33701b4 100644 --- a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx +++ b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx @@ -21,13 +21,18 @@ import * as columnFactories from './columns'; import { componentEntityColumns, systemEntityColumns } from './presets'; import { Table, TableColumn } from '@backstage/core-components'; -type Props = { +/** + * Props for {@link EntityTable}. + * + * @public + */ +export interface EntityTableProps { title: string; variant?: 'gridItem'; entities: T[]; emptyContent?: ReactNode; columns: TableColumn[]; -}; +} const useStyles = makeStyles(theme => ({ empty: { @@ -37,13 +42,21 @@ const useStyles = makeStyles(theme => ({ }, })); -export function EntityTable({ - entities, - title, - emptyContent, - variant = 'gridItem', - columns, -}: Props) { +/** + * A general entity table component, that can be used for composing more + * specific entity tables. + * + * @public + */ +export function EntityTable(props: EntityTableProps) { + const { + entities, + title, + emptyContent, + variant = 'gridItem', + columns, + } = props; + const classes = useStyles(); const tableStyle: React.CSSProperties = { minWidth: '0', diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index 1f4f5ee0d0..f6198fa1dc 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -29,11 +29,11 @@ import { formatEntityRefTitle, } from '../EntityRefLink'; -export function createEntityRefColumn({ - defaultKind, -}: { +/** @public */ +export function createEntityRefColumn(options: { defaultKind?: string; }): TableColumn { + const { defaultKind } = options; function formatContent(entity: T): string { return ( entity.metadata?.title || @@ -49,7 +49,7 @@ export function createEntityRefColumn({ customFilterAndSearch(filter, entity) { // TODO: We could implement this more efficiently, like searching over // each field that is displayed individually (kind, namespace, name). - // but that migth confuse the user as it will behave different than a + // but that might confuse the user as it will behave different than a // simple text search. // Another alternative would be to cache the values. But writing them // into the entity feels bad too. @@ -70,6 +70,7 @@ export function createEntityRefColumn({ }; } +/** @public */ export function createEntityRelationColumn({ title, relation, @@ -110,6 +111,7 @@ export function createEntityRelationColumn({ }; } +/** @public */ export function createOwnerColumn(): TableColumn { return createEntityRelationColumn({ title: 'Owner', @@ -118,6 +120,7 @@ export function createOwnerColumn(): TableColumn { }); } +/** @public */ export function createDomainColumn(): TableColumn { return createEntityRelationColumn({ title: 'Domain', @@ -129,6 +132,7 @@ export function createDomainColumn(): TableColumn { }); } +/** @public */ export function createSystemColumn(): TableColumn { return createEntityRelationColumn({ title: 'System', @@ -140,6 +144,7 @@ export function createSystemColumn(): TableColumn { }); } +/** @public */ export function createMetadataDescriptionColumn< T extends Entity, >(): TableColumn { @@ -157,6 +162,7 @@ export function createMetadataDescriptionColumn< }; } +/** @public */ export function createSpecLifecycleColumn(): TableColumn { return { title: 'Lifecycle', @@ -164,6 +170,7 @@ export function createSpecLifecycleColumn(): TableColumn { }; } +/** @public */ export function createSpecTypeColumn(): TableColumn { return { title: 'Type', diff --git a/plugins/catalog-react/src/components/EntityTable/index.ts b/plugins/catalog-react/src/components/EntityTable/index.ts index 36203e7929..2ed07f9823 100644 --- a/plugins/catalog-react/src/components/EntityTable/index.ts +++ b/plugins/catalog-react/src/components/EntityTable/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { EntityTable } from './EntityTable'; +export type { EntityTableProps } from './EntityTable'; diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index a25c50c0a9..9f51a5df68 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -46,6 +46,7 @@ const useStyles = makeStyles( const icon = ; const checkedIcon = ; +/** @public */ export const EntityTagPicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index 225193dc93..a0770ef30a 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -22,12 +22,18 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { Select } from '@backstage/core-components'; -export type EntityTypeFilterProps = { +/** + * Props for {@link EntityTypePicker}. + * + * @public + */ +export interface EntityTypePickerProps { initialFilter?: string; hidden?: boolean; -}; +} -export const EntityTypePicker = (props: EntityTypeFilterProps) => { +/** @public */ +export const EntityTypePicker = (props: EntityTypePickerProps) => { const { hidden, initialFilter } = props; const alertApi = useApi(alertApiRef); const { error, availableTypes, selectedTypes, setSelectedTypes } = diff --git a/plugins/catalog-react/src/components/EntityTypePicker/index.ts b/plugins/catalog-react/src/components/EntityTypePicker/index.ts index 1124b5c30c..03351337b7 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/index.ts +++ b/plugins/catalog-react/src/components/EntityTypePicker/index.ts @@ -15,4 +15,4 @@ */ export { EntityTypePicker } from './EntityTypePicker'; -export type { EntityTypeFilterProps } from './EntityTypePicker'; +export type { EntityTypePickerProps } from './EntityTypePicker'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index 0977e86d58..b80472a81d 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export * from './EntityKindPicker'; export * from './EntityLifecyclePicker'; export * from './EntityOwnerPicker'; diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index dfbd91f76b..1ac43d242d 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -218,7 +218,9 @@ const NO_WARNING_PACKAGES = [ 'packages/types', 'packages/version-bridge', 'plugins/catalog-backend-module-ldap', + 'plugins/catalog-backend-module-msgraph', 'plugins/catalog-common', + 'plugins/catalog-graph', 'plugins/permission-backend', 'plugins/permission-common', 'plugins/permission-node', From 2bd5f240439f63a75a310b7627a55629f2309006 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 3 Feb 2022 22:14:29 +0100 Subject: [PATCH 02/35] fix(scaffolder-backend): use the right key when initializing a Gitlab client Signed-off-by: djamaile --- .changeset/seven-teachers-arrive.md | 5 +++++ .../src/scaffolder/actions/builtin/publish/gitlab.ts | 3 ++- .../scaffolder/actions/builtin/publish/gitlabMergeRequest.ts | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/seven-teachers-arrive.md diff --git a/.changeset/seven-teachers-arrive.md b/.changeset/seven-teachers-arrive.md new file mode 100644 index 0000000000..b9d6264576 --- /dev/null +++ b/.changeset/seven-teachers-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +fix for the gitlab:publish action to use the `oauthToken` key when creating a Gilab client. This only happens if ctx.input.token is provided else the key `token` will be used. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index a04766f941..48979306b1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -112,10 +112,11 @@ export function createPublishGitlabAction(options: { } const token = ctx.input.token || integrationConfig.config.token!; + const tokenType = ctx.input.token ? 'oauthToken' : 'token'; const client = new Gitlab({ host: integrationConfig.config.baseUrl, - token, + [tokenType]: token, }); let { id: targetNamespace } = (await client.Namespaces.show(owner)) as { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 281b72789f..bf687268c3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -118,10 +118,11 @@ export const createPublishGitlabMergeRequestAction = (options: { } const token = ctx.input.token ?? integrationConfig.config.token!; + const tokenType = ctx.input.token ? 'oauthToken' : 'token'; const api = new Gitlab({ host: integrationConfig.config.baseUrl, - token, + [tokenType]: token, }); const fileRoot = ctx.workspacePath; From 1049a6d94924304ded900520823e0567b4f29134 Mon Sep 17 00:00:00 2001 From: djamaile Date: Thu, 3 Feb 2022 22:21:22 +0100 Subject: [PATCH 03/35] fix(scaffolder-backend): spelling mistakes Signed-off-by: djamaile --- .changeset/seven-teachers-arrive.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/seven-teachers-arrive.md b/.changeset/seven-teachers-arrive.md index b9d6264576..bdbbc33c44 100644 --- a/.changeset/seven-teachers-arrive.md +++ b/.changeset/seven-teachers-arrive.md @@ -2,4 +2,5 @@ '@backstage/plugin-scaffolder-backend': patch --- -fix for the gitlab:publish action to use the `oauthToken` key when creating a Gilab client. This only happens if ctx.input.token is provided else the key `token` will be used. +fix for the `gitlab:publish` action to use the `oauthToken` key when creating a +`Gitlab` client. This only happens if `ctx.input.token` is provided else the key `token` will be used. From 323f48704d434fcc406754ed6e798d6f77776bf7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Feb 2022 18:09:30 +0100 Subject: [PATCH 04/35] todo: switch to routable extension + add test Signed-off-by: Patrik Oldsberg --- .changeset/chilly-pans-jog.md | 5 +++ plugins/todo/api-report.md | 8 ++++- plugins/todo/package.json | 1 + plugins/todo/src/plugin.test.ts | 22 ------------- plugins/todo/src/plugin.test.tsx | 56 ++++++++++++++++++++++++++++++++ plugins/todo/src/plugin.ts | 14 ++++---- 6 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 .changeset/chilly-pans-jog.md delete mode 100644 plugins/todo/src/plugin.test.ts create mode 100644 plugins/todo/src/plugin.test.tsx diff --git a/.changeset/chilly-pans-jog.md b/.changeset/chilly-pans-jog.md new file mode 100644 index 0000000000..b63d5fde50 --- /dev/null +++ b/.changeset/chilly-pans-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-todo': minor +--- + +**BREAKING**: The `EntityTodoContent` is now a routable extension. This means it must be rendered within a route, but that's most likely already the case for most apps. The mount point `RouteRef` is available via `todoPlugin.routes.entityContent`. diff --git a/plugins/todo/api-report.md b/plugins/todo/api-report.md index 0784f5d56e..c1667f1111 100644 --- a/plugins/todo/api-report.md +++ b/plugins/todo/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; // @public export const EntityTodoContent: () => JSX.Element; @@ -79,5 +80,10 @@ export type TodoListResult = { }; // @public -export const todoPlugin: BackstagePlugin<{}, {}>; +export const todoPlugin: BackstagePlugin< + { + entityContent: RouteRef; + }, + {} +>; ``` diff --git a/plugins/todo/package.json b/plugins/todo/package.json index fc9893fcd9..fb433992a1 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -52,6 +52,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", + "react-router": "6.0.0-beta.0", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/todo/src/plugin.test.ts b/plugins/todo/src/plugin.test.ts deleted file mode 100644 index a99373abc6..0000000000 --- a/plugins/todo/src/plugin.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { todoPlugin } from './plugin'; - -describe('todo', () => { - it('should export plugin', () => { - expect(todoPlugin).toBeDefined(); - }); -}); diff --git a/plugins/todo/src/plugin.test.tsx b/plugins/todo/src/plugin.test.tsx new file mode 100644 index 0000000000..57b9be291d --- /dev/null +++ b/plugins/todo/src/plugin.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Route } from 'react-router'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { todoPlugin, EntityTodoContent } from './plugin'; +import { todoApiRef } from './api'; + +describe('todo', () => { + it('should export plugin', () => { + expect(todoPlugin).toBeDefined(); + }); + + it('should render EntityTodoContent', async () => { + const rendered = await renderInTestApp( + ({ + items: [ + { + tag: 'FIXME', + text: 'Make sure this test works', + }, + ], + limit: 10, + offset: 0, + totalCount: 1, + }), + }, + ], + ]} + > + } /> + , + ); + + await expect(rendered.findByText('FIXME')).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/todo/src/plugin.ts b/plugins/todo/src/plugin.ts index a7d3702f2c..d74acfb02c 100644 --- a/plugins/todo/src/plugin.ts +++ b/plugins/todo/src/plugin.ts @@ -17,10 +17,11 @@ import { todoApiRef, TodoClient } from './api'; import { createApiFactory, createPlugin, - createComponentExtension, + createRoutableExtension, discoveryApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { rootRouteRef } from './routes'; /** * The Todo plugin instance. @@ -42,7 +43,7 @@ export const todoPlugin = createPlugin({ }), ], routes: { - // root: rootRouteRef, + entityContent: rootRouteRef, }, }); @@ -52,12 +53,9 @@ export const todoPlugin = createPlugin({ * @public */ export const EntityTodoContent = todoPlugin.provide( - createComponentExtension({ + createRoutableExtension({ name: 'EntityTodoContent', - component: { - lazy: () => import('./components/TodoList').then(m => m.TodoList), - }, - // TODO(Rugvip): Switch back to routable extension once apps are migrated - // mountPoint: rootRouteRef, + component: () => import('./components/TodoList').then(m => m.TodoList), + mountPoint: rootRouteRef, }), ); From bbbaa8ed61b4e6b5485aa77cd079ea88acc4b501 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Feb 2022 19:03:20 +0100 Subject: [PATCH 05/35] cli: no longer diff dev/ or src/ Signed-off-by: Patrik Oldsberg --- .changeset/big-jeans-love.md | 5 +++++ packages/cli/src/commands/plugin/diff.ts | 9 ++------- 2 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 .changeset/big-jeans-love.md diff --git a/.changeset/big-jeans-love.md b/.changeset/big-jeans-love.md new file mode 100644 index 0000000000..e97ae15ff4 --- /dev/null +++ b/.changeset/big-jeans-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `plugin:diff` command no longer validates the existence of any of the files within `dev/` or `src/`. diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts index 6a0127794f..153bd16597 100644 --- a/packages/cli/src/commands/plugin/diff.ts +++ b/packages/cli/src/commands/plugin/diff.ts @@ -39,18 +39,13 @@ const fileHandlers = [ patterns: ['package.json'], handler: handlers.packageJson, }, - { - // Not all plugins have routes - patterns: ['src/routes.ts'], - handler: handlers.skip, - }, { // make sure files in 1st level of src/ and dev/ exist - patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/], + patterns: ['.eslintrc.js'], handler: handlers.exists, }, { - patterns: ['README.md', 'tsconfig.json', /^src\//], + patterns: ['README.md', 'tsconfig.json', /^src\//, /^dev\//], handler: handlers.skip, }, ]; From 18411594901331edbb7817dd597d501245bc1f27 Mon Sep 17 00:00:00 2001 From: iammnils Date: Fri, 4 Feb 2022 11:44:55 +0100 Subject: [PATCH 06/35] chore: add sda codeowner Signed-off-by: iammnils --- .github/CODEOWNERS | 73 +++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 08057d5af6..eaba7ef3e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,42 +5,49 @@ # https://help.github.com/articles/about-codeowners/ * @backstage/reviewers -/docs/features/techdocs @backstage/techdocs-core -/docs/features/search @backstage/techdocs-core -/docs/assets/search @backstage/techdocs-core -/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps -/plugins/circleci @backstage/reviewers @adamdmharvey -/plugins/code-coverage @backstage/reviewers @alde @nissayeva -/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva -/plugins/cost-insights @backstage/silver-lining -/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 @backstage/reviewers @yacut -/plugins/home @backstage/techdocs-core -/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin -/plugins/jenkins @backstage/reviewers @timja -/plugins/jenkins-backend @backstage/reviewers @timja -/plugins/kafka @backstage/reviewers @nirga -/plugins/kafka-backend @backstage/reviewers @nirga -/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka -/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski -/plugins/git-release-manager @backstage/reviewers @erikengervall -/tech-insights-backend @backstage/reviewers @xantier @iain-b -/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b -/packages/search-common @backstage/techdocs-core -/packages/techdocs-cli @backstage/techdocs-core -/packages/techdocs-cli-embedded-app @backstage/techdocs-core -/packages/techdocs-common @backstage/techdocs-core /.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining /.changeset/search-* @backstage/techdocs-core /.changeset/techdocs-* @backstage/techdocs-core /cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core +/docs/assets/search @backstage/techdocs-core +/docs/features/search @backstage/techdocs-core +/docs/features/techdocs @backstage/techdocs-core +/packages/search-common @backstage/techdocs-core +/packages/techdocs-cli @backstage/techdocs-core +/packages/techdocs-cli-embedded-app @backstage/techdocs-core +/packages/techdocs-common @backstage/techdocs-core +/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps /plugins/apache-airflow @backstage/reviewers @cmpadden +/plugins/api-docs @backstage/reviewers @backstage/sda-se-reviewers +/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin +/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin +/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin +/plugins/bitrise @backstage/reviewers @backstage/sda-se-reviewers +/plugins/catalog-graph @backstage/reviewers @backstage/sda-se-reviewers +/plugins/circleci @backstage/reviewers @adamdmharvey +/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios +/plugins/code-coverage @backstage/reviewers @alde @nissayeva +/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva +/plugins/cost-insights @backstage/silver-lining +/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers +/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers +/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers +/plugins/git-release-manager @backstage/reviewers @erikengervall +/plugins/home @backstage/techdocs-core +/plugins/ilert @backstage/reviewers @yacut +/plugins/jenkins @backstage/reviewers @timja +/plugins/jenkins-backend @backstage/reviewers @timja +/plugins/kafka @backstage/reviewers @nirga +/plugins/kafka-backend @backstage/reviewers @nirga /plugins/newrelic-dashboard @backstage/reviewers @mufaddal7 +/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski +/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka +/plugins/search @backstage/techdocs-core +/plugins/search-* @backstage/techdocs-core +/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers +/plugins/techdocs @backstage/techdocs-core +/plugins/techdocs-backend @backstage/techdocs-core +/tech-insights-backend @backstage/reviewers @xantier @iain-b +/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b +/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b +/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b From 0a2719a5ab7c0205f8e70a2ce96e82c0b6b31199 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 21 Jan 2022 01:52:05 +0100 Subject: [PATCH 07/35] cli: add initial role types and detection Signed-off-by: Patrik Oldsberg --- packages/cli/package.json | 3 +- .../src/lib/role/detectPackageRole.test.ts | 295 ++++++++++++++++++ .../cli/src/lib/role/detectPackageRole.ts | 117 +++++++ packages/cli/src/lib/role/index.ts | 22 ++ packages/cli/src/lib/role/types.ts | 34 ++ 5 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/lib/role/detectPackageRole.test.ts create mode 100644 packages/cli/src/lib/role/detectPackageRole.ts create mode 100644 packages/cli/src/lib/role/index.ts create mode 100644 packages/cli/src/lib/role/types.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index aad08a21eb..41cefe6c53 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -112,7 +112,8 @@ "webpack-node-externals": "^3.0.0", "yaml": "^1.10.0", "yml-loader": "^2.1.0", - "yn": "^4.0.0" + "yn": "^4.0.0", + "zod": "^3.11.6" }, "devDependencies": { "@backstage/backend-common": "^0.10.6", diff --git a/packages/cli/src/lib/role/detectPackageRole.test.ts b/packages/cli/src/lib/role/detectPackageRole.test.ts new file mode 100644 index 0000000000..f513a5aa3a --- /dev/null +++ b/packages/cli/src/lib/role/detectPackageRole.test.ts @@ -0,0 +1,295 @@ +/* + * Copyright 2022 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 { detectPackageRole } from './detectPackageRole'; + +describe('detectPackageRole', () => { + it('detects explicit package roles', () => { + expect( + detectPackageRole({ + backstage: { + role: 'web-library', + }, + }), + ).toEqual({ + role: 'web-library', + platform: 'web', + }); + + expect( + detectPackageRole({ + backstage: { + role: 'app', + }, + }), + ).toEqual({ + role: 'app', + platform: 'web', + }); + + expect(() => + detectPackageRole({ + name: 'test', + backstage: {}, + }), + ).toThrow('Package test must specify a role in the "backstage" field'); + + expect(() => + detectPackageRole({ + name: 'test', + backstage: { role: 'invalid' }, + }), + ).toThrow(`Unknown role 'invalid' in package test`); + }); + + it('detects the role of example-app', () => { + expect( + detectPackageRole({ + name: 'example-app', + private: true, + bundled: true, + scripts: { + start: 'backstage-cli app:serve', + build: 'backstage-cli app:build', + clean: 'backstage-cli clean', + test: 'backstage-cli test', + 'test:e2e': + 'start-server-and-test start http://localhost:3000 cy:dev', + 'test:e2e:ci': + 'start-server-and-test start http://localhost:3000 cy:run', + lint: 'backstage-cli lint', + 'cy:dev': 'cypress open', + 'cy:run': 'cypress run', + }, + }), + ).toEqual({ + role: 'app', + platform: 'web', + }); + }); + + it('detects the role of example-backend', () => { + expect( + detectPackageRole({ + name: 'example-backend', + main: 'dist/index.cjs.js', + types: 'src/index.ts', + scripts: { + build: 'backstage-cli backend:bundle', + 'build-image': + 'docker build ../.. -f Dockerfile --tag example-backend', + start: 'backstage-cli backend:dev', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + clean: 'backstage-cli clean', + 'migrate:create': 'knex migrate:make -x ts', + }, + }), + ).toEqual({ + role: 'backend', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-catalog', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog', + main: 'src/index.ts', + types: 'src/index.ts', + 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', + }, + }), + ).toEqual({ + role: 'plugin-frontend', + platform: 'web', + }); + }); + + it('detects the role of @backstage/plugin-catalog-backend', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-backend', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + start: 'backstage-cli backend:dev', + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'plugin-backend', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-catalog-react', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-react', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'web-library', + platform: 'web', + }); + }); + + it('detects the role of @backstage/plugin-catalog-common', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-common', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + module: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli build', + lint: 'backstage-cli lint', + test: 'backstage-cli test --passWithNoTests', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'common-library', + platform: 'common', + }); + }); + + it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-catalog-backend-module-ldap', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'plugin-backend-module', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-permission-node', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-permission-node', + main: 'src/index.ts', + types: 'src/index.ts', + homepage: 'https://backstage.io', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual({ + role: 'node-library', + platform: 'node', + }); + }); + + it('detects the role of @backstage/plugin-analytics-module-ga', () => { + expect( + detectPackageRole({ + name: '@backstage/plugin-analytics-module-ga', + main: 'src/index.ts', + types: 'src/index.ts', + 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', + }, + }), + ).toEqual({ + role: 'plugin-frontend-module', + platform: 'web', + }); + }); +}); diff --git a/packages/cli/src/lib/role/detectPackageRole.ts b/packages/cli/src/lib/role/detectPackageRole.ts new file mode 100644 index 0000000000..f0941c1e42 --- /dev/null +++ b/packages/cli/src/lib/role/detectPackageRole.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import { PackageRoleInfo } from './types'; + +const packageRoles: PackageRoleInfo[] = [ + { role: 'app', platform: 'web' }, + { role: 'backend', platform: 'node' }, + { role: 'cli', platform: 'node' }, + { role: 'web-library', platform: 'web' }, + { role: 'node-library', platform: 'node' }, + { role: 'common-library', platform: 'common' }, + { role: 'plugin-frontend', platform: 'web' }, + { role: 'plugin-frontend-module', platform: 'web' }, + { role: 'plugin-backend', platform: 'node' }, + { role: 'plugin-backend-module', platform: 'node' }, +]; +const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); + +const backstagePackageSchema = z.object({ + name: z.string().optional(), + scripts: z + .object({ + start: z.string().optional(), + build: z.string().optional(), + }) + .optional(), + backstage: z + .object({ + role: z.string().optional(), + }) + .optional(), + publishConfig: z + .object({ + main: z.string().optional(), + types: z.string().optional(), + module: z.string().optional(), + }) + .optional(), + main: z.string().optional(), + types: z.string().optional(), + module: z.string().optional(), +}); + +export function detectPackageRole( + pkgJson: unknown, +): PackageRoleInfo | undefined { + const pkg = backstagePackageSchema.parse(pkgJson); + + // If there's an explicit role, use that. + if (pkg.backstage) { + const { role } = pkg.backstage; + if (!role) { + throw new Error( + `Package ${pkg.name} must specify a role in the "backstage" field`, + ); + } + + const roleInfo = packageRoles.find(r => r.role === role); + if (!roleInfo) { + throw new Error(`Unknown role '${role}' in package ${pkg.name}`); + } + return roleInfo; + } + + if (pkg.scripts?.start?.includes('app:serve')) { + return roleMap.app; + } + if (pkg.scripts?.build?.includes('backend:bundle')) { + return roleMap.backend; + } + if (pkg.name?.includes('plugin') && pkg.name?.includes('backend-module')) { + return roleMap['plugin-backend-module']; + } + if (pkg.name?.includes('plugin') && pkg.name?.includes('module')) { + return roleMap['plugin-frontend-module']; + } + if (pkg.scripts?.start?.includes('plugin:serve')) { + return roleMap['plugin-frontend']; + } + if (pkg.scripts?.start?.includes('backend:dev')) { + return roleMap['plugin-backend']; + } + + const mainEntry = pkg.publishConfig?.main || pkg.main; + const moduleEntry = pkg.publishConfig?.module || pkg.module; + const typesEntry = pkg.publishConfig?.types || pkg.types; + if (typesEntry) { + if (mainEntry && moduleEntry) { + return roleMap['common-library']; + } + if (moduleEntry || mainEntry?.endsWith('.esm.js')) { + return roleMap['web-library']; + } + if (mainEntry) { + return roleMap['node-library']; + } + } else if (mainEntry) { + return roleMap.cli; + } + + return undefined; +} diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts new file mode 100644 index 0000000000..41d65073d4 --- /dev/null +++ b/packages/cli/src/lib/role/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { + PackageRoleInfo, + PackagePlatform, + PackageRoleName, +} from './types'; +export { detectPackageRole } from './detectPackageRole'; diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts new file mode 100644 index 0000000000..1f5afe7926 --- /dev/null +++ b/packages/cli/src/lib/role/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type PackageRoleName = + | 'app' + | 'backend' + | 'cli' + | 'web-library' + | 'node-library' + | 'common-library' + | 'plugin-frontend' + | 'plugin-frontend-module' + | 'plugin-backend' + | 'plugin-backend-module'; + +export type PackagePlatform = 'node' | 'web' | 'common'; + +export interface PackageRoleInfo { + role: PackageRoleName; + platform: PackagePlatform; +} From 4e62242eb1945cd9c9f969144a4e72a451953aa7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 14:03:49 +0100 Subject: [PATCH 08/35] cli: split role detection into read and detect Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/role/index.ts | 2 +- ...ckageRole.test.ts => packageRoles.test.ts} | 16 +++--- .../{detectPackageRole.ts => packageRoles.ts} | 56 +++++++++++-------- 3 files changed, 43 insertions(+), 31 deletions(-) rename packages/cli/src/lib/role/{detectPackageRole.test.ts => packageRoles.test.ts} (97%) rename packages/cli/src/lib/role/{detectPackageRole.ts => packageRoles.ts} (92%) diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 41d65073d4..45ddbc8823 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -19,4 +19,4 @@ export type { PackagePlatform, PackageRoleName, } from './types'; -export { detectPackageRole } from './detectPackageRole'; +export { detectPackageRole, readPackageRole } from './packageRoles'; diff --git a/packages/cli/src/lib/role/detectPackageRole.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts similarity index 97% rename from packages/cli/src/lib/role/detectPackageRole.test.ts rename to packages/cli/src/lib/role/packageRoles.test.ts index f513a5aa3a..555dc9ce9c 100644 --- a/packages/cli/src/lib/role/detectPackageRole.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { detectPackageRole } from './detectPackageRole'; +import { readPackageRole, detectPackageRole } from './packageRoles'; -describe('detectPackageRole', () => { - it('detects explicit package roles', () => { +describe('readPackageRole', () => { + it('reads explicit package roles', () => { expect( - detectPackageRole({ + readPackageRole({ backstage: { role: 'web-library', }, @@ -30,7 +30,7 @@ describe('detectPackageRole', () => { }); expect( - detectPackageRole({ + readPackageRole({ backstage: { role: 'app', }, @@ -41,20 +41,22 @@ describe('detectPackageRole', () => { }); expect(() => - detectPackageRole({ + readPackageRole({ name: 'test', backstage: {}, }), ).toThrow('Package test must specify a role in the "backstage" field'); expect(() => - detectPackageRole({ + readPackageRole({ name: 'test', backstage: { role: 'invalid' }, }), ).toThrow(`Unknown role 'invalid' in package test`); }); +}); +describe('detectPackageRole', () => { it('detects the role of example-app', () => { expect( detectPackageRole({ diff --git a/packages/cli/src/lib/role/detectPackageRole.ts b/packages/cli/src/lib/role/packageRoles.ts similarity index 92% rename from packages/cli/src/lib/role/detectPackageRole.ts rename to packages/cli/src/lib/role/packageRoles.ts index f0941c1e42..3525cbd314 100644 --- a/packages/cli/src/lib/role/detectPackageRole.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -31,7 +31,38 @@ const packageRoles: PackageRoleInfo[] = [ ]; const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); -const backstagePackageSchema = z.object({ +const readSchema = z.object({ + name: z.string().optional(), + backstage: z + .object({ + role: z.string().optional(), + }) + .optional(), +}); + +export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { + const pkg = readSchema.parse(pkgJson); + + // If there's an explicit role, use that. + if (pkg.backstage) { + const { role } = pkg.backstage; + if (!role) { + throw new Error( + `Package ${pkg.name} must specify a role in the "backstage" field`, + ); + } + + const roleInfo = packageRoles.find(r => r.role === role); + if (!roleInfo) { + throw new Error(`Unknown role '${role}' in package ${pkg.name}`); + } + return roleInfo; + } + + return undefined; +} + +const detectionSchema = z.object({ name: z.string().optional(), scripts: z .object({ @@ -39,11 +70,6 @@ const backstagePackageSchema = z.object({ build: z.string().optional(), }) .optional(), - backstage: z - .object({ - role: z.string().optional(), - }) - .optional(), publishConfig: z .object({ main: z.string().optional(), @@ -59,23 +85,7 @@ const backstagePackageSchema = z.object({ export function detectPackageRole( pkgJson: unknown, ): PackageRoleInfo | undefined { - const pkg = backstagePackageSchema.parse(pkgJson); - - // If there's an explicit role, use that. - if (pkg.backstage) { - const { role } = pkg.backstage; - if (!role) { - throw new Error( - `Package ${pkg.name} must specify a role in the "backstage" field`, - ); - } - - const roleInfo = packageRoles.find(r => r.role === role); - if (!roleInfo) { - throw new Error(`Unknown role '${role}' in package ${pkg.name}`); - } - return roleInfo; - } + const pkg = detectionSchema.parse(pkgJson); if (pkg.scripts?.start?.includes('app:serve')) { return roleMap.app; From 8263cac40e2b9e7b90b7146b0e73b9cb0b98b33c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 14:53:20 +0100 Subject: [PATCH 09/35] cli: bit more specific matching of module roles Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/role/packageRoles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 3525cbd314..76467d990e 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -93,10 +93,10 @@ export function detectPackageRole( if (pkg.scripts?.build?.includes('backend:bundle')) { return roleMap.backend; } - if (pkg.name?.includes('plugin') && pkg.name?.includes('backend-module')) { + if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) { return roleMap['plugin-backend-module']; } - if (pkg.name?.includes('plugin') && pkg.name?.includes('module')) { + if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { return roleMap['plugin-frontend-module']; } if (pkg.scripts?.start?.includes('plugin:serve')) { From 29260100446370472eaa10cbf2af4e11b5ff8936 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 15:36:06 +0100 Subject: [PATCH 10/35] cli: add new role-based bundle command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/bundle/bundleApp.ts | 39 ++++++++++ .../cli/src/commands/bundle/bundleBackend.ts | 75 +++++++++++++++++++ packages/cli/src/commands/bundle/command.ts | 46 ++++++++++++ packages/cli/src/commands/bundle/index.ts | 17 +++++ packages/cli/src/commands/index.ts | 14 ++++ 5 files changed, 191 insertions(+) create mode 100644 packages/cli/src/commands/bundle/bundleApp.ts create mode 100644 packages/cli/src/commands/bundle/bundleBackend.ts create mode 100644 packages/cli/src/commands/bundle/command.ts create mode 100644 packages/cli/src/commands/bundle/index.ts diff --git a/packages/cli/src/commands/bundle/bundleApp.ts b/packages/cli/src/commands/bundle/bundleApp.ts new file mode 100644 index 0000000000..1ebd1f044e --- /dev/null +++ b/packages/cli/src/commands/bundle/bundleApp.ts @@ -0,0 +1,39 @@ +/* + * 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 fs from 'fs-extra'; +import { buildBundle } from '../../lib/bundler'; +import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; +import { loadCliConfig } from '../../lib/config'; +import { paths } from '../../lib/paths'; + +interface BundleAppOptions { + writeStats: boolean; + configPaths: string[]; +} + +export async function bundleApp(options: BundleAppOptions) { + const { name } = await fs.readJson(paths.resolveTarget('package.json')); + await buildBundle({ + entry: 'src/index', + parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), + statsJsonEnabled: options.writeStats, + ...(await loadCliConfig({ + args: options.configPaths, + fromPackage: name, + })), + }); +} diff --git a/packages/cli/src/commands/bundle/bundleBackend.ts b/packages/cli/src/commands/bundle/bundleBackend.ts new file mode 100644 index 0000000000..0f240d87dc --- /dev/null +++ b/packages/cli/src/commands/bundle/bundleBackend.ts @@ -0,0 +1,75 @@ +/* + * 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 os from 'os'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import tar, { CreateOptions } from 'tar'; +import { createDistWorkspace } from '../../lib/packager'; +import { paths } from '../../lib/paths'; +import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; +import { buildPackage, Output } from '../../lib/builder'; + +const BUNDLE_FILE = 'bundle.tar.gz'; +const SKELETON_FILE = 'skeleton.tar.gz'; + +interface BundleBackendOptions { + skipBuildDependencies: boolean; +} + +export async function bundleBackend(options: BundleBackendOptions) { + const targetDir = paths.resolveTarget('dist'); + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + + // We build the target package without generating type declarations. + await buildPackage({ outputs: new Set([Output.cjs]) }); + + const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-bundle')); + try { + await createDistWorkspace([pkg.name], { + targetDir: tmpDir, + buildDependencies: !options.skipBuildDependencies, + buildExcludes: [pkg.name], + parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), + skeleton: SKELETON_FILE, + }); + + // We built the target backend package using the regular build process, but the result of + // that has now been packed into the dist workspace, so clean up the dist dir. + await fs.remove(targetDir); + await fs.mkdir(targetDir); + + // Move out skeleton.tar.gz before we create the main bundle, no point having that included up twice. + await fs.move( + resolvePath(tmpDir, SKELETON_FILE), + resolvePath(targetDir, SKELETON_FILE), + ); + + // Create main bundle.tar.gz, with some tweaks to make it more likely hit Docker build cache. + await tar.create( + { + file: resolvePath(targetDir, BUNDLE_FILE), + cwd: tmpDir, + portable: true, + noMtime: true, + gzip: true, + } as CreateOptions & { noMtime: boolean }, + [''], + ); + } finally { + await fs.remove(tmpDir); + } +} diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts new file mode 100644 index 0000000000..a13faeee62 --- /dev/null +++ b/packages/cli/src/commands/bundle/command.ts @@ -0,0 +1,46 @@ +/* + * 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 fs from 'fs-extra'; +import { Command } from 'commander'; +import { paths } from '../../lib/paths'; +import { readPackageRole } from '../../lib/role/packageRoles'; +import { bundleApp } from './bundleApp'; +import { bundleBackend } from './bundleBackend'; + +export async function command(cmd: Command): Promise { + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const roleInfo = readPackageRole(pkg); + if (!roleInfo) { + throw new Error(`Target package must have 'backstage.role' set`); + } + + const options = { + configPaths: cmd.config as string[], + writeStats: Boolean(cmd.stats), + skipBuildDependencies: Boolean(cmd.skipBuildDependencies), + }; + + if (roleInfo.role === 'app') { + return bundleApp(options); + } else if (roleInfo.role === 'backend') { + return bundleBackend(options); + } + + throw new Error( + `Bundle command is not supported for package role '${roleInfo.role}'`, + ); +} diff --git a/packages/cli/src/commands/bundle/index.ts b/packages/cli/src/commands/bundle/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/bundle/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 362771f8f2..e834e0e494 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -133,6 +133,20 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./build').then(m => m.default))); + program + .command('bundle') + .description('Bundle a package for deployment') + .option( + '--skip-build-dependencies', + 'Skip the automatic building of local dependencies', + ) + .option( + '--stats', + 'If bundle stats are available, write them to the output directory', + ) + .option(...configOption) + .action(lazy(() => import('./bundle').then(m => m.command))); + program .command('lint') .option( From 17a9f90efcd0a4d9cfc9c93d24755b94ca71437c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 16:06:56 +0100 Subject: [PATCH 11/35] cli: add util to get role info by name Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/role/index.ts | 6 ++++- .../cli/src/lib/role/packageRoles.test.ts | 26 +++++++++++++++++-- packages/cli/src/lib/role/packageRoles.ts | 14 ++++++---- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 45ddbc8823..5a1f836036 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -19,4 +19,8 @@ export type { PackagePlatform, PackageRoleName, } from './types'; -export { detectPackageRole, readPackageRole } from './packageRoles'; +export { + getRoleInfo, + detectPackageRole, + readPackageRole, +} from './packageRoles'; diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index 555dc9ce9c..d41cd08488 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -14,7 +14,29 @@ * limitations under the License. */ -import { readPackageRole, detectPackageRole } from './packageRoles'; +import { + getRoleInfo, + readPackageRole, + detectPackageRole, +} from './packageRoles'; + +describe('getRoleInfo', () => { + it('provides role info by role', () => { + expect(getRoleInfo('web-library')).toEqual({ + role: 'web-library', + platform: 'web', + }); + + expect(getRoleInfo('app')).toEqual({ + role: 'app', + platform: 'web', + }); + + expect(() => getRoleInfo('invalid')).toThrow( + `Unknown package role 'invalid'`, + ); + }); +}); describe('readPackageRole', () => { it('reads explicit package roles', () => { @@ -52,7 +74,7 @@ describe('readPackageRole', () => { name: 'test', backstage: { role: 'invalid' }, }), - ).toThrow(`Unknown role 'invalid' in package test`); + ).toThrow(`Unknown package role 'invalid'`); }); }); diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 76467d990e..9d0ec501c6 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -31,6 +31,14 @@ const packageRoles: PackageRoleInfo[] = [ ]; const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); +export function getRoleInfo(role: string): PackageRoleInfo { + const roleInfo = packageRoles.find(r => r.role === role); + if (!roleInfo) { + throw new Error(`Unknown package role '${role}'`); + } + return roleInfo; +} + const readSchema = z.object({ name: z.string().optional(), backstage: z @@ -52,11 +60,7 @@ export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { ); } - const roleInfo = packageRoles.find(r => r.role === role); - if (!roleInfo) { - throw new Error(`Unknown role '${role}' in package ${pkg.name}`); - } - return roleInfo; + return getRoleInfo(role); } return undefined; From f0ee50cfabd467c357ed955dc1ce19684a9abcb2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 16:39:48 +0100 Subject: [PATCH 12/35] cli: add utility for passing explicit role option Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/bundle/command.ts | 10 +---- packages/cli/src/commands/index.ts | 3 +- packages/cli/src/lib/role/index.ts | 3 +- .../cli/src/lib/role/packageRoles.test.ts | 44 +++++++++++++++++++ packages/cli/src/lib/role/packageRoles.ts | 18 ++++++++ 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts index a13faeee62..44b39e6797 100644 --- a/packages/cli/src/commands/bundle/command.ts +++ b/packages/cli/src/commands/bundle/command.ts @@ -14,19 +14,13 @@ * limitations under the License. */ -import fs from 'fs-extra'; import { Command } from 'commander'; -import { paths } from '../../lib/paths'; -import { readPackageRole } from '../../lib/role/packageRoles'; import { bundleApp } from './bundleApp'; import { bundleBackend } from './bundleBackend'; +import { readRoleForCommand } from '../../lib/role'; export async function command(cmd: Command): Promise { - const pkg = await fs.readJson(paths.resolveTarget('package.json')); - const roleInfo = readPackageRole(pkg); - if (!roleInfo) { - throw new Error(`Target package must have 'backstage.role' set`); - } + const roleInfo = await readRoleForCommand(cmd); const options = { configPaths: cmd.config as string[], diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index e834e0e494..860cd0fd62 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -136,6 +136,8 @@ export function registerCommands(program: CommanderStatic) { program .command('bundle') .description('Bundle a package for deployment') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') .option( '--skip-build-dependencies', 'Skip the automatic building of local dependencies', @@ -144,7 +146,6 @@ export function registerCommands(program: CommanderStatic) { '--stats', 'If bundle stats are available, write them to the output directory', ) - .option(...configOption) .action(lazy(() => import('./bundle').then(m => m.command))); program diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 5a1f836036..9becfa367b 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -21,6 +21,7 @@ export type { } from './types'; export { getRoleInfo, - detectPackageRole, readPackageRole, + readRoleForCommand, + detectPackageRole, } from './packageRoles'; diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index d41cd08488..f3302d3256 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -14,9 +14,12 @@ * limitations under the License. */ +import mockFs from 'mock-fs'; +import { Command } from 'commander'; import { getRoleInfo, readPackageRole, + readRoleForCommand, detectPackageRole, } from './packageRoles'; @@ -78,6 +81,47 @@ describe('readPackageRole', () => { }); }); +describe('readRoleForCommand', () => { + function mkCommand(args: string) { + return new Command() + .option('--role ', 'test role') + .parse(['node', 'entry.js', ...args.split(' ')]) as Command; + } + + beforeEach(() => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'test', + backstage: { + role: 'web-library', + }, + }), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('provides role info by role', async () => { + await expect(readRoleForCommand(mkCommand(''))).resolves.toEqual({ + role: 'web-library', + platform: 'web', + }); + + await expect( + readRoleForCommand(mkCommand('--role node-library')), + ).resolves.toEqual({ + role: 'node-library', + platform: 'node', + }); + + await expect( + readRoleForCommand(mkCommand('--role invalid')), + ).rejects.toThrow(`Unknown package role 'invalid'`); + }); +}); + describe('detectPackageRole', () => { it('detects the role of example-app', () => { expect( diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 9d0ec501c6..67bee0e64f 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -15,6 +15,9 @@ */ import { z } from 'zod'; +import fs from 'fs-extra'; +import { Command } from 'commander'; +import { paths } from '../paths'; import { PackageRoleInfo } from './types'; const packageRoles: PackageRoleInfo[] = [ @@ -66,6 +69,21 @@ export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { return undefined; } +export async function readRoleForCommand( + cmd: Command, +): Promise { + if (cmd.role) { + return getRoleInfo(cmd.role); + } + + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const info = readPackageRole(pkg); + if (!info) { + throw new Error(`Target package must have 'backstage.role' set`); + } + return info; +} + const detectionSchema = z.object({ name: z.string().optional(), scripts: z From 9227753a7c30f57c7988dfa0a0e0b8098129aba1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 17:17:23 +0100 Subject: [PATCH 13/35] cli: added role-based start command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 13 ++++ packages/cli/src/commands/start/command.ts | 53 +++++++++++++ packages/cli/src/commands/start/index.ts | 17 +++++ .../cli/src/commands/start/startBackend.ts | 41 ++++++++++ .../cli/src/commands/start/startFrontend.ts | 76 +++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 packages/cli/src/commands/start/command.ts create mode 100644 packages/cli/src/commands/start/index.ts create mode 100644 packages/cli/src/commands/start/startBackend.ts create mode 100644 packages/cli/src/commands/start/startFrontend.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 860cd0fd62..cecbab82e5 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -148,6 +148,19 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./bundle').then(m => m.command))); + program + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .action(lazy(() => import('./start').then(m => m.command))); + program .command('lint') .option( diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts new file mode 100644 index 0000000000..ff4d3b7c0b --- /dev/null +++ b/packages/cli/src/commands/start/command.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command } from 'commander'; +import { startBackend } from './startBackend'; +import { startFrontend } from './startFrontend'; +import { readRoleForCommand } from '../../lib/role'; + +export async function command(cmd: Command): Promise { + const roleInfo = await readRoleForCommand(cmd); + + const options = { + configPaths: cmd.config as string[], + checksEnabled: Boolean(cmd.check), + inspectEnabled: Boolean(cmd.inspect), + inspectBrkEnabled: Boolean(cmd.inspectBrk), + }; + + switch (roleInfo.role) { + case 'backend': + case 'plugin-backend': + case 'plugin-backend-module': + case 'node-library': + return startBackend(options); + case 'app': + return startFrontend({ + ...options, + entry: 'src/index', + verifyVersions: true, + }); + case 'web-library': + case 'plugin-frontend': + case 'plugin-frontend-module': + return startFrontend({ entry: 'dev/index', ...options }); + default: + throw new Error( + `Start command is not supported for package role '${roleInfo.role}'`, + ); + } +} diff --git a/packages/cli/src/commands/start/index.ts b/packages/cli/src/commands/start/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/start/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { command } from './command'; diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts new file mode 100644 index 0000000000..61ada7a291 --- /dev/null +++ b/packages/cli/src/commands/start/startBackend.ts @@ -0,0 +1,41 @@ +/* + * 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 fs from 'fs-extra'; +import { paths } from '../../lib/paths'; +import { serveBackend } from '../../lib/bundler'; + +interface StartBackendOptions { + checksEnabled: boolean; + inspectEnabled: boolean; + inspectBrkEnabled: boolean; +} + +export async function startBackend(options: StartBackendOptions) { + // Cleaning dist/ before we start the dev process helps work around an issue + // where we end up with the entrypoint executing multiple times, causing + // a port bind conflict among other things. + await fs.remove(paths.resolveTarget('dist')); + + const waitForExit = await serveBackend({ + entry: 'src/index', + checksEnabled: options.checksEnabled, + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); +} diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts new file mode 100644 index 0000000000..e5500c310b --- /dev/null +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -0,0 +1,76 @@ +/* + * 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 fs from 'fs-extra'; +import chalk from 'chalk'; +import uniq from 'lodash/uniq'; +import { serveBundle } from '../../lib/bundler'; +import { loadCliConfig } from '../../lib/config'; +import { paths } from '../../lib/paths'; +import { Lockfile } from '../../lib/versioning'; +import { includedFilter } from '../versions/lint'; + +interface StartAppOptions { + verifyVersions?: boolean; + entry: string; + + checksEnabled: boolean; + configPaths: string[]; +} + +export async function startFrontend(options: StartAppOptions) { + if (options.verifyVersions) { + const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + const result = lockfile.analyze({ + filter: includedFilter, + }); + const problemPackages = [...result.newVersions, ...result.newRanges].map( + ({ name }) => name, + ); + + if (problemPackages.length > 1) { + console.log( + chalk.yellow( + `⚠️ Some of the following packages may be outdated or have duplicate installations: + + ${uniq(problemPackages).join(', ')} + `, + ), + ); + console.log( + chalk.yellow( + `⚠️ This can be resolved using the following command: + + yarn backstage-cli versions:check --fix + `, + ), + ); + } + } + + const { name } = await fs.readJson(paths.resolveTarget('package.json')); + const waitForExit = await serveBundle({ + entry: options.entry, + checksEnabled: options.checksEnabled, + ...(await loadCliConfig({ + args: options.configPaths, + fromPackage: name, + withFilteredKeys: true, + })), + }); + + await waitForExit(); +} From c7169dc440f0560d945be95ef8e541f9ea6b4f50 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 22 Jan 2022 17:52:30 +0100 Subject: [PATCH 14/35] cli: add migrate:package-role command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 5 ++ .../cli/src/commands/migrate/packageRole.ts | 69 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/cli/src/commands/migrate/packageRole.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index cecbab82e5..61ded0741c 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -233,6 +233,11 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); + program + .command('migrate:package-role') + .description(`Add package role field to packages that don't have it`) + .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); + program .command('versions:bump') .option( diff --git a/packages/cli/src/commands/migrate/packageRole.ts b/packages/cli/src/commands/migrate/packageRole.ts new file mode 100644 index 0000000000..86898a6012 --- /dev/null +++ b/packages/cli/src/commands/migrate/packageRole.ts @@ -0,0 +1,69 @@ +/* + * 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 fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { getPackages } from '@manypkg/get-packages'; +import { paths } from '../../lib/paths'; +import { readPackageRole, detectPackageRole } from '../../lib/role'; + +export default async () => { + const { packages } = await getPackages(paths.targetDir); + + await Promise.all( + packages.map(async ({ dir, packageJson: pkg }) => { + const { name } = pkg; + const existingRole = readPackageRole(pkg); + if (existingRole) { + return; + } + + const detectedRole = detectPackageRole(pkg); + if (!detectedRole) { + console.error(`No role detected for package ${name}`); + return; + } + + console.log(`Detected package role of ${name} as ${detectedRole.role}`); + + let newPkg = pkg as any; + + const pkgKeys = Object.keys(pkg); + if (pkgKeys.includes('backstage')) { + newPkg.backstage = { + ...newPkg.backstage, + role: detectedRole.role, + }; + } else { + // We insert the backstage field after one of these fields, otherwise at the end + const index = + Math.max( + pkgKeys.indexOf('version'), + pkgKeys.indexOf('private'), + pkgKeys.indexOf('publishConfig'), + ) + 1 || pkgKeys.length; + + const pkgEntries = Object.entries(pkg); + pkgEntries.splice(index, 0, ['backstage', { role: detectedRole.role }]); + newPkg = Object.fromEntries(pkgEntries); + } + + await fs.writeJson(resolvePath(dir, 'package.json'), newPkg, { + spaces: 2, + }); + }), + ); +}; From 0bf983e703cef5c7fb3c98ee0dc32d9918800b6e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 13:05:54 +0100 Subject: [PATCH 15/35] cli: add PackageGraph utility for listing extended packages Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/monorepo/PackageGraph.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index 9860b92ef6..db7a528494 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { Package } from '@manypkg/get-packages'; +import { getPackages, Package } from '@manypkg/get-packages'; +import { paths } from '../paths'; +import { PackageRoleName } from '../role'; type PackageJSON = Package['packageJson']; @@ -25,8 +27,17 @@ export interface ExtendedPackageJSON extends PackageJSON { // The `bundled` field is a field known within Backstage, it means // that the package bundles all of its dependencies in its build output. bundled?: boolean; + + backstage?: { + role?: PackageRoleName; + }; } +export type ExtendedPackage = { + dir: string; + packageJson: ExtendedPackageJSON; +}; + export type PackageGraphNode = { /** The name of the package */ name: string; @@ -47,6 +58,11 @@ export type PackageGraphNode = { }; export class PackageGraph extends Map { + static async listTargetPackages(): Promise { + const { packages } = await getPackages(paths.targetDir); + return packages as ExtendedPackage[]; + } + static fromPackages(packages: Package[]): PackageGraph { const graph = new PackageGraph(); From 189c44104c05ac3a727cd3002e3813bc746a85d7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 13:07:21 +0100 Subject: [PATCH 16/35] cli: added migrate:package-scripts Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 7 ++ .../src/commands/migrate/packageScripts.ts | 70 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 packages/cli/src/commands/migrate/packageScripts.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 61ded0741c..ef2e5ef96a 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -238,6 +238,13 @@ export function registerCommands(program: CommanderStatic) { .description(`Add package role field to packages that don't have it`) .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); + program + .command('migrate:package-scripts') + .description('Set package scripts according to each package role') + .action( + lazy(() => import('./migrate/packageScripts').then(m => m.command)), + ); + program .command('versions:bump') .option( diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts new file mode 100644 index 0000000000..58a8481c81 --- /dev/null +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -0,0 +1,70 @@ +/* + * 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 fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { PackageGraph } from '../../lib/monorepo'; +import { readPackageRole, PackageRoleName } from '../../lib/role'; + +const bundledRoles: PackageRoleName[] = ['app', 'backend']; +const noStartRoles: PackageRoleName[] = ['cli', 'common-library']; + +export async function command() { + const packages = await PackageGraph.listTargetPackages(); + + await Promise.all( + packages.map(async ({ dir, packageJson }) => { + const roleInfo = readPackageRole(packageJson); + if (!roleInfo) { + return; + } + + const hasStart = !noStartRoles.includes(roleInfo.role); + const isBundled = bundledRoles.includes(roleInfo.role); + + const expectedScripts = { + ...(hasStart && { start: 'backstage-cli start' }), + ...(isBundled + ? { bundle: 'backstage-cli bundle' } + : { build: 'backstage-cli build' }), + lint: 'backstage-cli lint', + test: 'backstage-cli test', + clean: 'backstage-cli clean', + ...(!isBundled && { + postpack: 'backstage-cli postpack', + prepack: 'backstage-cli prepack', + }), + }; + + let changed = false; + const currentScripts = (packageJson.scripts = packageJson.scripts || {}); + + for (const [name, value] of Object.entries(expectedScripts)) { + if (currentScripts[name] !== value) { + changed = true; + currentScripts[name] = value; + } + } + + if (changed) { + console.log(`Updating scripts for ${packageJson.name}`); + await fs.writeJson(resolvePath(dir, 'package.json'), packageJson, { + spaces: 2, + }); + } + }), + ); +} From 703314d1a53c5515f960ce43bfb2a0cac0bcfff2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 14:28:13 +0100 Subject: [PATCH 17/35] cli: move new commands into experimental sub-commands Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 72 ++++++++++--------- .../src/commands/migrate/packageScripts.ts | 4 +- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ef2e5ef96a..caeee1557c 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -133,34 +133,6 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./build').then(m => m.default))); - program - .command('bundle') - .description('Bundle a package for deployment') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory', - ) - .action(lazy(() => import('./bundle').then(m => m.command))); - - program - .command('start') - .description('Start a package for local development') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option('--check', 'Enable type checking and linting if available') - .option('--inspect', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .action(lazy(() => import('./start').then(m => m.command))); - program .command('lint') .option( @@ -233,13 +205,49 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); - program - .command('migrate:package-role') + const script = program + .command('script [command]', { hidden: true }) + .description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]'); + + script + .command('bundle') + .description('Bundle a package for deployment') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option( + '--skip-build-dependencies', + 'Skip the automatic building of local dependencies', + ) + .option( + '--stats', + 'If bundle stats are available, write them to the output directory', + ) + .action(lazy(() => import('./bundle').then(m => m.command))); + + script + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .action(lazy(() => import('./start').then(m => m.command))); + + const migrate = program + .command('migrate [command]', { hidden: true }) + .description('Migration utilities [EXPERIMENTAL]'); + + migrate + .command('package-role') .description(`Add package role field to packages that don't have it`) .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); - program - .command('migrate:package-scripts') + migrate + .command('package-scripts') .description('Set package scripts according to each package role') .action( lazy(() => import('./migrate/packageScripts').then(m => m.command)), diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index 58a8481c81..ada358b56b 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -36,9 +36,9 @@ export async function command() { const isBundled = bundledRoles.includes(roleInfo.role); const expectedScripts = { - ...(hasStart && { start: 'backstage-cli start' }), + ...(hasStart && { start: 'backstage-cli script start' }), ...(isBundled - ? { bundle: 'backstage-cli bundle' } + ? { bundle: 'backstage-cli script bundle' } : { build: 'backstage-cli build' }), lint: 'backstage-cli lint', test: 'backstage-cli test', From 38963aa860feced0673f514d00d901667d3c6d9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 15:47:53 +0100 Subject: [PATCH 18/35] cli: add new role-based build command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/command.ts | 52 +++++++++++++++++++ packages/cli/src/commands/build/index.ts | 17 ++++++ packages/cli/src/commands/index.ts | 9 +++- .../src/commands/{build.ts => oldBuild.ts} | 0 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/commands/build/command.ts create mode 100644 packages/cli/src/commands/build/index.ts rename packages/cli/src/commands/{build.ts => oldBuild.ts} (100%) diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts new file mode 100644 index 0000000000..733faa9480 --- /dev/null +++ b/packages/cli/src/commands/build/command.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command } from 'commander'; +import { buildPackage, Output } from '../../lib/builder'; +import { PackageRoleName, readRoleForCommand } from '../../lib/role'; + +const bundledRoles: PackageRoleName[] = ['app', 'backend']; + +const esmPlatforms = ['web', 'common']; +const cjsPlatforms = ['node', 'common']; + +export async function command(cmd: Command): Promise { + const roleInfo = await readRoleForCommand(cmd); + + if (bundledRoles.includes(roleInfo.role)) { + throw new Error( + `Build command is not supported for package role '${roleInfo.role}'`, + ); + } + + const outputs = new Set(); + + if (cjsPlatforms.includes(roleInfo.platform)) { + outputs.add(Output.cjs); + } + if (esmPlatforms.includes(roleInfo.platform)) { + outputs.add(Output.esm); + } + if (roleInfo.role !== 'cli') { + outputs.add(Output.types); + } + + await buildPackage({ + outputs, + minify: Boolean(cmd.minify), + useApiExtractor: Boolean(cmd.experimentalTypeBuild), + }); +} diff --git a/packages/cli/src/commands/build/index.ts b/packages/cli/src/commands/build/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/build/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index caeee1557c..ea0f7dec58 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -131,7 +131,7 @@ export function registerCommands(program: CommanderStatic) { .option('--outputs ', 'List of formats to output [types,cjs,esm]') .option('--minify', 'Minify the generated code') .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./build').then(m => m.default))); + .action(lazy(() => import('./oldBuild').then(m => m.default))); program .command('lint') @@ -224,6 +224,13 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./bundle').then(m => m.command))); + script + .command('build') + .description('Build a package for publishing') + .option('--minify', 'Minify the generated code') + .option('--experimental-type-build', 'Enable experimental type build') + .action(lazy(() => import('./build').then(m => m.command))); + script .command('start') .description('Start a package for local development') diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/oldBuild.ts similarity index 100% rename from packages/cli/src/commands/build.ts rename to packages/cli/src/commands/oldBuild.ts From 036d95b99b4d9cec1ec40ebd87d2b6e5526503e0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 16:19:45 +0100 Subject: [PATCH 19/35] cli: refactored registration of script and migration commands Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 162 ++++++++++++++++++----------- 1 file changed, 101 insertions(+), 61 deletions(-) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ea0f7dec58..54fe48e2cf 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -18,14 +18,106 @@ import { assertError } from '@backstage/errors'; import { CommanderStatic } from 'commander'; import { exitWithError } from '../lib/errors'; -export function registerCommands(program: CommanderStatic) { - const configOption = [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => [...opts, opt], - Array(), - ] as const; +const configOption = [ + '--config ', + 'Config files to load instead of app-config.yaml', + (opt: string, opts: string[]) => [...opts, opt], + Array(), +] as const; +export function registerScriptCommand(program: CommanderStatic) { + const command = program + .command('script [command]', { hidden: true }) + .description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]'); + + command + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .action(lazy(() => import('./start').then(m => m.command))); + + command + .command('build') + .description('Build a package for publishing') + .option('--minify', 'Minify the generated code') + .option('--experimental-type-build', 'Enable experimental type build') + .action(lazy(() => import('./build').then(m => m.command))); + + command + .command('bundle') + .description('Bundle a package for deployment') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option( + '--skip-build-dependencies', + 'Skip the automatic building of local dependencies', + ) + .option( + '--stats', + 'If bundle stats are available, write them to the output directory', + ) + .action(lazy(() => import('./bundle').then(m => m.command))); + + program + .command('lint') + .option( + '--format ', + 'Lint report output format', + 'eslint-formatter-friendly', + ) + .option('--fix', 'Attempt to automatically fix violations') + .description('Lint a package') + .action(lazy(() => import('./lint').then(m => m.default))); + + program + .command('test') + .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args + .helpOption(', --backstage-cli-help') // Let Jest handle help + .description('Run tests, forwarding args to Jest, defaulting to watch mode') + .action(lazy(() => import('./testCommand').then(m => m.default))); + + command + .command('clean') + .description('Delete cache directories') + .action(lazy(() => import('./clean/clean').then(m => m.default))); + + command + .command('prepack') + .description('Prepares a package for packaging before publishing') + .action(lazy(() => import('./pack').then(m => m.pre))); + + command + .command('postpack') + .description('Restores the changes made by the prepack command') + .action(lazy(() => import('./pack').then(m => m.post))); +} + +export function registerMigrateCommand(program: CommanderStatic) { + const command = program + .command('migrate [command]', { hidden: true }) + .description('Migration utilities [EXPERIMENTAL]'); + + command + .command('package-role') + .description(`Add package role field to packages that don't have it`) + .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); + + command + .command('package-scripts') + .description('Set package scripts according to each package role') + .action( + lazy(() => import('./migrate/packageScripts').then(m => m.command)), + ); +} + +export function registerCommands(program: CommanderStatic) { program .command('app:build') .description('Build an app for a production release') @@ -205,60 +297,8 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); - const script = program - .command('script [command]', { hidden: true }) - .description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]'); - - script - .command('bundle') - .description('Bundle a package for deployment') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory', - ) - .action(lazy(() => import('./bundle').then(m => m.command))); - - script - .command('build') - .description('Build a package for publishing') - .option('--minify', 'Minify the generated code') - .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./build').then(m => m.command))); - - script - .command('start') - .description('Start a package for local development') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') - .option('--check', 'Enable type checking and linting if available') - .option('--inspect', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .action(lazy(() => import('./start').then(m => m.command))); - - const migrate = program - .command('migrate [command]', { hidden: true }) - .description('Migration utilities [EXPERIMENTAL]'); - - migrate - .command('package-role') - .description(`Add package role field to packages that don't have it`) - .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); - - migrate - .command('package-scripts') - .description('Set package scripts according to each package role') - .action( - lazy(() => import('./migrate/packageScripts').then(m => m.command)), - ); + registerScriptCommand(program); + registerMigrateCommand(program); program .command('versions:bump') From c9f8c1189f57014c13a331bfa7687c0fbd12f83d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 16:20:27 +0100 Subject: [PATCH 20/35] cli: mark commands for planned deprecation Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 54fe48e2cf..c9c06141c3 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -118,6 +118,7 @@ export function registerMigrateCommand(program: CommanderStatic) { } export function registerCommands(program: CommanderStatic) { + // TODO(Rugvip): Deprecate in favor of script variant program .command('app:build') .description('Build an app for a production release') @@ -125,6 +126,7 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./app/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('app:serve') .description('Serve an app for local development') @@ -132,6 +134,7 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./app/serve').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:build') .description('Build a backend plugin') @@ -139,6 +142,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./backend/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:bundle') .description('Bundle the backend into a deployment archive') @@ -148,6 +152,7 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./backend/bundle').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:dev') .description('Start local development server with HMR for the backend') @@ -196,6 +201,7 @@ export function registerCommands(program: CommanderStatic) { lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), ); + // TODO(Rugvip): Deprecate in favor of script variant program .command('plugin:build') .description('Build a plugin') @@ -203,6 +209,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./plugin/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('plugin:serve') .description('Serves the dev/ folder of a plugin') @@ -217,6 +224,7 @@ export function registerCommands(program: CommanderStatic) { .description('Diff an existing plugin with the creation template') .action(lazy(() => import('./plugin/diff').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('build') .description('Build a package for publishing') @@ -225,6 +233,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./oldBuild').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('lint') .option( @@ -236,6 +245,7 @@ export function registerCommands(program: CommanderStatic) { .description('Lint a package') .action(lazy(() => import('./lint').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('test') .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args @@ -315,16 +325,19 @@ export function registerCommands(program: CommanderStatic) { .description('Check Backstage package versioning') .action(lazy(() => import('./versions/lint').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('prepack') .description('Prepares a package for packaging before publishing') .action(lazy(() => import('./pack').then(m => m.pre))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('postpack') .description('Restores the changes made by the prepack command') .action(lazy(() => import('./pack').then(m => m.post))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('clean') .description('Delete cache directories') From 3532a7d81c94d77b3c275685e0201a2e443d0a78 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 23 Jan 2022 16:33:46 +0100 Subject: [PATCH 21/35] cli: update package script migration to use script commands Signed-off-by: Patrik Oldsberg --- .../cli/src/commands/migrate/packageScripts.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index ada358b56b..5d0033606f 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -38,19 +38,20 @@ export async function command() { const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), ...(isBundled - ? { bundle: 'backstage-cli script bundle' } - : { build: 'backstage-cli build' }), - lint: 'backstage-cli lint', - test: 'backstage-cli test', - clean: 'backstage-cli clean', + ? { bundle: 'backstage-cli script bundle', build: undefined } + : { build: 'backstage-cli script build', bundle: undefined }), + lint: 'backstage-cli script lint', + test: 'backstage-cli script test', + clean: 'backstage-cli script clean', ...(!isBundled && { - postpack: 'backstage-cli postpack', - prepack: 'backstage-cli prepack', + postpack: 'backstage-cli script postpack', + prepack: 'backstage-cli script prepack', }), }; let changed = false; - const currentScripts = (packageJson.scripts = packageJson.scripts || {}); + const currentScripts: Record = + (packageJson.scripts = packageJson.scripts || {}); for (const [name, value] of Object.entries(expectedScripts)) { if (currentScripts[name] !== value) { From f2c5b7461773a2c9ced713c9bc3c86a83c912588 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 24 Jan 2022 00:34:20 +0100 Subject: [PATCH 22/35] cli: refactor role functions to work around simpler PackageRole Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/command.ts | 13 +- packages/cli/src/commands/bundle/command.ts | 12 +- .../cli/src/commands/migrate/packageRole.ts | 12 +- .../src/commands/migrate/packageScripts.ts | 14 +-- packages/cli/src/commands/start/command.ts | 8 +- packages/cli/src/lib/monorepo/PackageGraph.ts | 4 +- packages/cli/src/lib/role/index.ts | 12 +- .../cli/src/lib/role/packageRoles.test.ts | 111 ++++++------------ packages/cli/src/lib/role/packageRoles.ts | 63 +++++----- packages/cli/src/lib/role/types.ts | 5 +- 10 files changed, 106 insertions(+), 148 deletions(-) diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 733faa9480..5db6baa34f 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -16,19 +16,20 @@ import { Command } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -import { PackageRoleName, readRoleForCommand } from '../../lib/role'; +import { PackageRole, findRoleFromCommand, getRoleInfo } from '../../lib/role'; -const bundledRoles: PackageRoleName[] = ['app', 'backend']; +const bundledRoles: PackageRole[] = ['app', 'backend']; const esmPlatforms = ['web', 'common']; const cjsPlatforms = ['node', 'common']; export async function command(cmd: Command): Promise { - const roleInfo = await readRoleForCommand(cmd); + const role = await findRoleFromCommand(cmd); + const roleInfo = getRoleInfo(role); - if (bundledRoles.includes(roleInfo.role)) { + if (bundledRoles.includes(role)) { throw new Error( - `Build command is not supported for package role '${roleInfo.role}'`, + `Build command is not supported for package role '${role}'`, ); } @@ -40,7 +41,7 @@ export async function command(cmd: Command): Promise { if (esmPlatforms.includes(roleInfo.platform)) { outputs.add(Output.esm); } - if (roleInfo.role !== 'cli') { + if (role !== 'cli') { outputs.add(Output.types); } diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts index 44b39e6797..4b8c718001 100644 --- a/packages/cli/src/commands/bundle/command.ts +++ b/packages/cli/src/commands/bundle/command.ts @@ -17,10 +17,10 @@ import { Command } from 'commander'; import { bundleApp } from './bundleApp'; import { bundleBackend } from './bundleBackend'; -import { readRoleForCommand } from '../../lib/role'; +import { findRoleFromCommand } from '../../lib/role'; export async function command(cmd: Command): Promise { - const roleInfo = await readRoleForCommand(cmd); + const role = await findRoleFromCommand(cmd); const options = { configPaths: cmd.config as string[], @@ -28,13 +28,11 @@ export async function command(cmd: Command): Promise { skipBuildDependencies: Boolean(cmd.skipBuildDependencies), }; - if (roleInfo.role === 'app') { + if (role === 'app') { return bundleApp(options); - } else if (roleInfo.role === 'backend') { + } else if (role === 'backend') { return bundleBackend(options); } - throw new Error( - `Bundle command is not supported for package role '${roleInfo.role}'`, - ); + throw new Error(`Bundle command is not supported for package role '${role}'`); } diff --git a/packages/cli/src/commands/migrate/packageRole.ts b/packages/cli/src/commands/migrate/packageRole.ts index 86898a6012..e8bfb8e7c8 100644 --- a/packages/cli/src/commands/migrate/packageRole.ts +++ b/packages/cli/src/commands/migrate/packageRole.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { getPackages } from '@manypkg/get-packages'; import { paths } from '../../lib/paths'; -import { readPackageRole, detectPackageRole } from '../../lib/role'; +import { getRoleFromPackage, detectRoleFromPackage } from '../../lib/role'; export default async () => { const { packages } = await getPackages(paths.targetDir); @@ -26,18 +26,18 @@ export default async () => { await Promise.all( packages.map(async ({ dir, packageJson: pkg }) => { const { name } = pkg; - const existingRole = readPackageRole(pkg); + const existingRole = getRoleFromPackage(pkg); if (existingRole) { return; } - const detectedRole = detectPackageRole(pkg); + const detectedRole = detectRoleFromPackage(pkg); if (!detectedRole) { console.error(`No role detected for package ${name}`); return; } - console.log(`Detected package role of ${name} as ${detectedRole.role}`); + console.log(`Detected package role of ${name} as ${detectedRole}`); let newPkg = pkg as any; @@ -45,7 +45,7 @@ export default async () => { if (pkgKeys.includes('backstage')) { newPkg.backstage = { ...newPkg.backstage, - role: detectedRole.role, + role: detectedRole, }; } else { // We insert the backstage field after one of these fields, otherwise at the end @@ -57,7 +57,7 @@ export default async () => { ) + 1 || pkgKeys.length; const pkgEntries = Object.entries(pkg); - pkgEntries.splice(index, 0, ['backstage', { role: detectedRole.role }]); + pkgEntries.splice(index, 0, ['backstage', { role: detectedRole }]); newPkg = Object.fromEntries(pkgEntries); } diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index 5d0033606f..c54d4ebb31 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -17,23 +17,23 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { PackageGraph } from '../../lib/monorepo'; -import { readPackageRole, PackageRoleName } from '../../lib/role'; +import { getRoleFromPackage, PackageRole } from '../../lib/role'; -const bundledRoles: PackageRoleName[] = ['app', 'backend']; -const noStartRoles: PackageRoleName[] = ['cli', 'common-library']; +const bundledRoles: PackageRole[] = ['app', 'backend']; +const noStartRoles: PackageRole[] = ['cli', 'common-library']; export async function command() { const packages = await PackageGraph.listTargetPackages(); await Promise.all( packages.map(async ({ dir, packageJson }) => { - const roleInfo = readPackageRole(packageJson); - if (!roleInfo) { + const role = getRoleFromPackage(packageJson); + if (!role) { return; } - const hasStart = !noStartRoles.includes(roleInfo.role); - const isBundled = bundledRoles.includes(roleInfo.role); + const hasStart = !noStartRoles.includes(role); + const isBundled = bundledRoles.includes(role); const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index ff4d3b7c0b..6615d5fd46 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -17,10 +17,10 @@ import { Command } from 'commander'; import { startBackend } from './startBackend'; import { startFrontend } from './startFrontend'; -import { readRoleForCommand } from '../../lib/role'; +import { findRoleFromCommand } from '../../lib/role'; export async function command(cmd: Command): Promise { - const roleInfo = await readRoleForCommand(cmd); + const role = await findRoleFromCommand(cmd); const options = { configPaths: cmd.config as string[], @@ -29,7 +29,7 @@ export async function command(cmd: Command): Promise { inspectBrkEnabled: Boolean(cmd.inspectBrk), }; - switch (roleInfo.role) { + switch (role) { case 'backend': case 'plugin-backend': case 'plugin-backend-module': @@ -47,7 +47,7 @@ export async function command(cmd: Command): Promise { return startFrontend({ entry: 'dev/index', ...options }); default: throw new Error( - `Start command is not supported for package role '${roleInfo.role}'`, + `Start command is not supported for package role '${role}'`, ); } } diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index db7a528494..89d5f79cd2 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -16,7 +16,7 @@ import { getPackages, Package } from '@manypkg/get-packages'; import { paths } from '../paths'; -import { PackageRoleName } from '../role'; +import { PackageRole } from '../role'; type PackageJSON = Package['packageJson']; @@ -29,7 +29,7 @@ export interface ExtendedPackageJSON extends PackageJSON { bundled?: boolean; backstage?: { - role?: PackageRoleName; + role?: PackageRole; }; } diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index 9becfa367b..a01be8d11e 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -14,14 +14,10 @@ * limitations under the License. */ -export type { - PackageRoleInfo, - PackagePlatform, - PackageRoleName, -} from './types'; +export type { PackageRoleInfo, PackagePlatform, PackageRole } from './types'; export { getRoleInfo, - readPackageRole, - readRoleForCommand, - detectPackageRole, + getRoleFromPackage, + findRoleFromCommand, + detectRoleFromPackage, } from './packageRoles'; diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index f3302d3256..db79143e50 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -18,9 +18,9 @@ import mockFs from 'mock-fs'; import { Command } from 'commander'; import { getRoleInfo, - readPackageRole, - readRoleForCommand, - detectPackageRole, + getRoleFromPackage, + findRoleFromCommand, + detectRoleFromPackage, } from './packageRoles'; describe('getRoleInfo', () => { @@ -28,11 +28,13 @@ describe('getRoleInfo', () => { expect(getRoleInfo('web-library')).toEqual({ role: 'web-library', platform: 'web', + bundled: false, }); expect(getRoleInfo('app')).toEqual({ role: 'app', platform: 'web', + bundled: true, }); expect(() => getRoleInfo('invalid')).toThrow( @@ -41,39 +43,33 @@ describe('getRoleInfo', () => { }); }); -describe('readPackageRole', () => { +describe('getRoleFromPackage', () => { it('reads explicit package roles', () => { expect( - readPackageRole({ + getRoleFromPackage({ backstage: { role: 'web-library', }, }), - ).toEqual({ - role: 'web-library', - platform: 'web', - }); + ).toEqual('web-library'); expect( - readPackageRole({ + getRoleFromPackage({ backstage: { role: 'app', }, }), - ).toEqual({ - role: 'app', - platform: 'web', - }); + ).toEqual('app'); expect(() => - readPackageRole({ + getRoleFromPackage({ name: 'test', backstage: {}, }), ).toThrow('Package test must specify a role in the "backstage" field'); expect(() => - readPackageRole({ + getRoleFromPackage({ name: 'test', backstage: { role: 'invalid' }, }), @@ -81,7 +77,7 @@ describe('readPackageRole', () => { }); }); -describe('readRoleForCommand', () => { +describe('findRoleFromCommand', () => { function mkCommand(args: string) { return new Command() .option('--role ', 'test role') @@ -104,28 +100,24 @@ describe('readRoleForCommand', () => { }); it('provides role info by role', async () => { - await expect(readRoleForCommand(mkCommand(''))).resolves.toEqual({ - role: 'web-library', - platform: 'web', - }); + await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual( + 'web-library', + ); await expect( - readRoleForCommand(mkCommand('--role node-library')), - ).resolves.toEqual({ - role: 'node-library', - platform: 'node', - }); + findRoleFromCommand(mkCommand('--role node-library')), + ).resolves.toEqual('node-library'); await expect( - readRoleForCommand(mkCommand('--role invalid')), + findRoleFromCommand(mkCommand('--role invalid')), ).rejects.toThrow(`Unknown package role 'invalid'`); }); }); -describe('detectPackageRole', () => { +describe('detectRoleFromPackage', () => { it('detects the role of example-app', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: 'example-app', private: true, bundled: true, @@ -143,15 +135,12 @@ describe('detectPackageRole', () => { 'cy:run': 'cypress run', }, }), - ).toEqual({ - role: 'app', - platform: 'web', - }); + ).toEqual('app'); }); it('detects the role of example-backend', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: 'example-backend', main: 'dist/index.cjs.js', types: 'src/index.ts', @@ -166,15 +155,12 @@ describe('detectPackageRole', () => { 'migrate:create': 'knex migrate:make -x ts', }, }), - ).toEqual({ - role: 'backend', - platform: 'node', - }); + ).toEqual('backend'); }); it('detects the role of @backstage/plugin-catalog', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog', main: 'src/index.ts', types: 'src/index.ts', @@ -194,15 +180,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-frontend', - platform: 'web', - }); + ).toEqual('plugin-frontend'); }); it('detects the role of @backstage/plugin-catalog-backend', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-backend', main: 'src/index.ts', types: 'src/index.ts', @@ -221,15 +204,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-backend', - platform: 'node', - }); + ).toEqual('plugin-backend'); }); it('detects the role of @backstage/plugin-catalog-react', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-react', main: 'src/index.ts', types: 'src/index.ts', @@ -247,15 +227,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'web-library', - platform: 'web', - }); + ).toEqual('web-library'); }); it('detects the role of @backstage/plugin-catalog-common', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-common', main: 'src/index.ts', types: 'src/index.ts', @@ -274,15 +251,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'common-library', - platform: 'common', - }); + ).toEqual('common-library'); }); it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-catalog-backend-module-ldap', main: 'src/index.ts', types: 'src/index.ts', @@ -300,15 +274,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-backend-module', - platform: 'node', - }); + ).toEqual('plugin-backend-module'); }); it('detects the role of @backstage/plugin-permission-node', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-permission-node', main: 'src/index.ts', types: 'src/index.ts', @@ -327,15 +298,12 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'node-library', - platform: 'node', - }); + ).toEqual('node-library'); }); it('detects the role of @backstage/plugin-analytics-module-ga', () => { expect( - detectPackageRole({ + detectRoleFromPackage({ name: '@backstage/plugin-analytics-module-ga', main: 'src/index.ts', types: 'src/index.ts', @@ -355,9 +323,6 @@ describe('detectPackageRole', () => { clean: 'backstage-cli clean', }, }), - ).toEqual({ - role: 'plugin-frontend-module', - platform: 'web', - }); + ).toEqual('plugin-frontend-module'); }); }); diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index 67bee0e64f..c12ca644b9 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -18,24 +18,23 @@ import { z } from 'zod'; import fs from 'fs-extra'; import { Command } from 'commander'; import { paths } from '../paths'; -import { PackageRoleInfo } from './types'; +import { PackageRole, PackageRoleInfo } from './types'; -const packageRoles: PackageRoleInfo[] = [ - { role: 'app', platform: 'web' }, - { role: 'backend', platform: 'node' }, - { role: 'cli', platform: 'node' }, - { role: 'web-library', platform: 'web' }, - { role: 'node-library', platform: 'node' }, - { role: 'common-library', platform: 'common' }, - { role: 'plugin-frontend', platform: 'web' }, - { role: 'plugin-frontend-module', platform: 'web' }, - { role: 'plugin-backend', platform: 'node' }, - { role: 'plugin-backend-module', platform: 'node' }, +const packageRoleInfos: PackageRoleInfo[] = [ + { role: 'app', bundled: true, platform: 'web' }, + { role: 'backend', bundled: true, platform: 'node' }, + { role: 'cli', bundled: false, platform: 'node' }, + { role: 'web-library', bundled: false, platform: 'web' }, + { role: 'node-library', bundled: false, platform: 'node' }, + { role: 'common-library', bundled: false, platform: 'common' }, + { role: 'plugin-frontend', bundled: false, platform: 'web' }, + { role: 'plugin-frontend-module', bundled: false, platform: 'web' }, + { role: 'plugin-backend', bundled: false, platform: 'node' }, + { role: 'plugin-backend-module', bundled: false, platform: 'node' }, ]; -const roleMap = Object.fromEntries(packageRoles.map(i => [i.role, i])); export function getRoleInfo(role: string): PackageRoleInfo { - const roleInfo = packageRoles.find(r => r.role === role); + const roleInfo = packageRoleInfos.find(r => r.role === role); if (!roleInfo) { throw new Error(`Unknown package role '${role}'`); } @@ -51,7 +50,7 @@ const readSchema = z.object({ .optional(), }); -export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { +export function getRoleFromPackage(pkgJson: unknown): PackageRole | undefined { const pkg = readSchema.parse(pkgJson); // If there's an explicit role, use that. @@ -63,21 +62,19 @@ export function readPackageRole(pkgJson: unknown): PackageRoleInfo | undefined { ); } - return getRoleInfo(role); + return getRoleInfo(role).role; } return undefined; } -export async function readRoleForCommand( - cmd: Command, -): Promise { +export async function findRoleFromCommand(cmd: Command): Promise { if (cmd.role) { - return getRoleInfo(cmd.role); + return getRoleInfo(cmd.role)?.role; } const pkg = await fs.readJson(paths.resolveTarget('package.json')); - const info = readPackageRole(pkg); + const info = getRoleFromPackage(pkg); if (!info) { throw new Error(`Target package must have 'backstage.role' set`); } @@ -104,28 +101,28 @@ const detectionSchema = z.object({ module: z.string().optional(), }); -export function detectPackageRole( +export function detectRoleFromPackage( pkgJson: unknown, -): PackageRoleInfo | undefined { +): PackageRole | undefined { const pkg = detectionSchema.parse(pkgJson); if (pkg.scripts?.start?.includes('app:serve')) { - return roleMap.app; + return 'app'; } if (pkg.scripts?.build?.includes('backend:bundle')) { - return roleMap.backend; + return 'backend'; } if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) { - return roleMap['plugin-backend-module']; + return 'plugin-backend-module'; } if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { - return roleMap['plugin-frontend-module']; + return 'plugin-frontend-module'; } if (pkg.scripts?.start?.includes('plugin:serve')) { - return roleMap['plugin-frontend']; + return 'plugin-frontend'; } if (pkg.scripts?.start?.includes('backend:dev')) { - return roleMap['plugin-backend']; + return 'plugin-backend'; } const mainEntry = pkg.publishConfig?.main || pkg.main; @@ -133,16 +130,16 @@ export function detectPackageRole( const typesEntry = pkg.publishConfig?.types || pkg.types; if (typesEntry) { if (mainEntry && moduleEntry) { - return roleMap['common-library']; + return 'common-library'; } if (moduleEntry || mainEntry?.endsWith('.esm.js')) { - return roleMap['web-library']; + return 'web-library'; } if (mainEntry) { - return roleMap['node-library']; + return 'node-library'; } } else if (mainEntry) { - return roleMap.cli; + return 'cli'; } return undefined; diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts index 1f5afe7926..efe2c99fd5 100644 --- a/packages/cli/src/lib/role/types.ts +++ b/packages/cli/src/lib/role/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export type PackageRoleName = +export type PackageRole = | 'app' | 'backend' | 'cli' @@ -29,6 +29,7 @@ export type PackageRoleName = export type PackagePlatform = 'node' | 'web' | 'common'; export interface PackageRoleInfo { - role: PackageRoleName; + role: PackageRole; + bundled: boolean; platform: PackagePlatform; } From 323562efc9236bd397034d8e24881fa15ea97065 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 25 Jan 2022 21:35:39 +0100 Subject: [PATCH 23/35] cli: switch to keeping track of outputs for each package role Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/build/command.ts | 9 +-- .../src/commands/migrate/packageScripts.ts | 6 +- packages/cli/src/lib/role/index.ts | 7 ++- .../cli/src/lib/role/packageRoles.test.ts | 4 +- packages/cli/src/lib/role/packageRoles.ts | 60 +++++++++++++++---- packages/cli/src/lib/role/types.ts | 3 +- 6 files changed, 66 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 5db6baa34f..b2e5925119 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -20,9 +20,6 @@ import { PackageRole, findRoleFromCommand, getRoleInfo } from '../../lib/role'; const bundledRoles: PackageRole[] = ['app', 'backend']; -const esmPlatforms = ['web', 'common']; -const cjsPlatforms = ['node', 'common']; - export async function command(cmd: Command): Promise { const role = await findRoleFromCommand(cmd); const roleInfo = getRoleInfo(role); @@ -35,13 +32,13 @@ export async function command(cmd: Command): Promise { const outputs = new Set(); - if (cjsPlatforms.includes(roleInfo.platform)) { + if (roleInfo.output.includes('cjs')) { outputs.add(Output.cjs); } - if (esmPlatforms.includes(roleInfo.platform)) { + if (roleInfo.output.includes('esm')) { outputs.add(Output.esm); } - if (role !== 'cli') { + if (roleInfo.output.includes('types')) { outputs.add(Output.types); } diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index c54d4ebb31..7a02411690 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -17,9 +17,8 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { PackageGraph } from '../../lib/monorepo'; -import { getRoleFromPackage, PackageRole } from '../../lib/role'; +import { getRoleFromPackage, getRoleInfo, PackageRole } from '../../lib/role'; -const bundledRoles: PackageRole[] = ['app', 'backend']; const noStartRoles: PackageRole[] = ['cli', 'common-library']; export async function command() { @@ -32,8 +31,9 @@ export async function command() { return; } + const roleInfo = getRoleInfo(role); const hasStart = !noStartRoles.includes(role); - const isBundled = bundledRoles.includes(role); + const isBundled = roleInfo.output.includes('bundle'); const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts index a01be8d11e..4e1047a628 100644 --- a/packages/cli/src/lib/role/index.ts +++ b/packages/cli/src/lib/role/index.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -export type { PackageRoleInfo, PackagePlatform, PackageRole } from './types'; +export type { + PackageRoleInfo, + PackagePlatform, + PackageOutputType, + PackageRole, +} from './types'; export { getRoleInfo, getRoleFromPackage, diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts index db79143e50..f6c9fb418a 100644 --- a/packages/cli/src/lib/role/packageRoles.test.ts +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -28,13 +28,13 @@ describe('getRoleInfo', () => { expect(getRoleInfo('web-library')).toEqual({ role: 'web-library', platform: 'web', - bundled: false, + output: ['types', 'esm'], }); expect(getRoleInfo('app')).toEqual({ role: 'app', platform: 'web', - bundled: true, + output: ['bundle'], }); expect(() => getRoleInfo('invalid')).toThrow( diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts index c12ca644b9..7c855ee0fd 100644 --- a/packages/cli/src/lib/role/packageRoles.ts +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -21,16 +21,56 @@ import { paths } from '../paths'; import { PackageRole, PackageRoleInfo } from './types'; const packageRoleInfos: PackageRoleInfo[] = [ - { role: 'app', bundled: true, platform: 'web' }, - { role: 'backend', bundled: true, platform: 'node' }, - { role: 'cli', bundled: false, platform: 'node' }, - { role: 'web-library', bundled: false, platform: 'web' }, - { role: 'node-library', bundled: false, platform: 'node' }, - { role: 'common-library', bundled: false, platform: 'common' }, - { role: 'plugin-frontend', bundled: false, platform: 'web' }, - { role: 'plugin-frontend-module', bundled: false, platform: 'web' }, - { role: 'plugin-backend', bundled: false, platform: 'node' }, - { role: 'plugin-backend-module', bundled: false, platform: 'node' }, + { + role: 'app', + platform: 'web', + output: ['bundle'], + }, + { + role: 'backend', + platform: 'node', + output: ['bundle'], + }, + { + role: 'cli', + platform: 'node', + output: ['cjs'], + }, + { + role: 'web-library', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'node-library', + platform: 'node', + output: ['types', 'cjs'], + }, + { + role: 'common-library', + platform: 'common', + output: ['types', 'esm', 'cjs'], + }, + { + role: 'plugin-frontend', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'plugin-frontend-module', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'plugin-backend', + platform: 'node', + output: ['types', 'cjs'], + }, + { + role: 'plugin-backend-module', + platform: 'node', + output: ['types', 'cjs'], + }, ]; export function getRoleInfo(role: string): PackageRoleInfo { diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts index efe2c99fd5..6a89f7bdaf 100644 --- a/packages/cli/src/lib/role/types.ts +++ b/packages/cli/src/lib/role/types.ts @@ -27,9 +27,10 @@ export type PackageRole = | 'plugin-backend-module'; export type PackagePlatform = 'node' | 'web' | 'common'; +export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs'; export interface PackageRoleInfo { role: PackageRole; - bundled: boolean; platform: PackagePlatform; + output: PackageOutputType[]; } From eaf67f05783da2a5c140f66ce19647daa9faf8b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 27 Jan 2022 18:43:50 +0100 Subject: [PATCH 24/35] changesets: add changset for cli role addition Signed-off-by: Patrik Oldsberg --- .changeset/brave-tools-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-tools-drop.md diff --git a/.changeset/brave-tools-drop.md b/.changeset/brave-tools-drop.md new file mode 100644 index 0000000000..02b6b7ebb1 --- /dev/null +++ b/.changeset/brave-tools-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Introduced initial support for an experimental `backstage.role` field in package.json, as well as experimental and hidden `migrate` and `script` sub-commands. We do not recommend usage of any of these additions yet. From 3941ada3cfa8dcf58c9939dd9ae56dd7cffd326d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Feb 2022 13:35:53 +0100 Subject: [PATCH 25/35] cli: remove separate bundle script, using build instead Signed-off-by: Patrik Oldsberg --- .../bundleApp.ts => build/buildApp.ts} | 4 +- .../buildBackend.ts} | 4 +- packages/cli/src/commands/build/command.ts | 25 +++++++----- packages/cli/src/commands/bundle/command.ts | 38 ------------------- packages/cli/src/commands/bundle/index.ts | 17 --------- packages/cli/src/commands/index.ts | 31 ++++++++------- .../src/commands/migrate/packageScripts.ts | 4 +- 7 files changed, 39 insertions(+), 84 deletions(-) rename packages/cli/src/commands/{bundle/bundleApp.ts => build/buildApp.ts} (93%) rename packages/cli/src/commands/{bundle/bundleBackend.ts => build/buildBackend.ts} (96%) delete mode 100644 packages/cli/src/commands/bundle/command.ts delete mode 100644 packages/cli/src/commands/bundle/index.ts diff --git a/packages/cli/src/commands/bundle/bundleApp.ts b/packages/cli/src/commands/build/buildApp.ts similarity index 93% rename from packages/cli/src/commands/bundle/bundleApp.ts rename to packages/cli/src/commands/build/buildApp.ts index 1ebd1f044e..3d86d796b7 100644 --- a/packages/cli/src/commands/bundle/bundleApp.ts +++ b/packages/cli/src/commands/build/buildApp.ts @@ -20,12 +20,12 @@ import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; -interface BundleAppOptions { +interface BuildAppOptions { writeStats: boolean; configPaths: string[]; } -export async function bundleApp(options: BundleAppOptions) { +export async function buildApp(options: BuildAppOptions) { const { name } = await fs.readJson(paths.resolveTarget('package.json')); await buildBundle({ entry: 'src/index', diff --git a/packages/cli/src/commands/bundle/bundleBackend.ts b/packages/cli/src/commands/build/buildBackend.ts similarity index 96% rename from packages/cli/src/commands/bundle/bundleBackend.ts rename to packages/cli/src/commands/build/buildBackend.ts index 0f240d87dc..a4d8858cf8 100644 --- a/packages/cli/src/commands/bundle/bundleBackend.ts +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -26,11 +26,11 @@ import { buildPackage, Output } from '../../lib/builder'; const BUNDLE_FILE = 'bundle.tar.gz'; const SKELETON_FILE = 'skeleton.tar.gz'; -interface BundleBackendOptions { +interface BuildBackendOptions { skipBuildDependencies: boolean; } -export async function bundleBackend(options: BundleBackendOptions) { +export async function buildBackend(options: BuildBackendOptions) { const targetDir = paths.resolveTarget('dist'); const pkg = await fs.readJson(paths.resolveTarget('package.json')); diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index b2e5925119..8c13b515d7 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -16,19 +16,26 @@ import { Command } from 'commander'; import { buildPackage, Output } from '../../lib/builder'; -import { PackageRole, findRoleFromCommand, getRoleInfo } from '../../lib/role'; - -const bundledRoles: PackageRole[] = ['app', 'backend']; +import { findRoleFromCommand, getRoleInfo } from '../../lib/role'; +import { buildApp } from './buildApp'; +import { buildBackend } from './buildBackend'; export async function command(cmd: Command): Promise { const role = await findRoleFromCommand(cmd); - const roleInfo = getRoleInfo(role); - if (bundledRoles.includes(role)) { - throw new Error( - `Build command is not supported for package role '${role}'`, - ); + if (role === 'app') { + return buildApp({ + configPaths: cmd.config as string[], + writeStats: Boolean(cmd.stats), + }); } + if (role === 'backend') { + return buildBackend({ + skipBuildDependencies: Boolean(cmd.skipBuildDependencies), + }); + } + + const roleInfo = getRoleInfo(role); const outputs = new Set(); @@ -42,7 +49,7 @@ export async function command(cmd: Command): Promise { outputs.add(Output.types); } - await buildPackage({ + return buildPackage({ outputs, minify: Boolean(cmd.minify), useApiExtractor: Boolean(cmd.experimentalTypeBuild), diff --git a/packages/cli/src/commands/bundle/command.ts b/packages/cli/src/commands/bundle/command.ts deleted file mode 100644 index 4b8c718001..0000000000 --- a/packages/cli/src/commands/bundle/command.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Command } from 'commander'; -import { bundleApp } from './bundleApp'; -import { bundleBackend } from './bundleBackend'; -import { findRoleFromCommand } from '../../lib/role'; - -export async function command(cmd: Command): Promise { - const role = await findRoleFromCommand(cmd); - - const options = { - configPaths: cmd.config as string[], - writeStats: Boolean(cmd.stats), - skipBuildDependencies: Boolean(cmd.skipBuildDependencies), - }; - - if (role === 'app') { - return bundleApp(options); - } else if (role === 'backend') { - return bundleBackend(options); - } - - throw new Error(`Bundle command is not supported for package role '${role}'`); -} diff --git a/packages/cli/src/commands/bundle/index.ts b/packages/cli/src/commands/bundle/index.ts deleted file mode 100644 index 680fe9e11d..0000000000 --- a/packages/cli/src/commands/bundle/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2022 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 { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index c9c06141c3..a33e7d4afe 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -45,25 +45,30 @@ export function registerScriptCommand(program: CommanderStatic) { command .command('build') - .description('Build a package for publishing') - .option('--minify', 'Minify the generated code') - .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./build').then(m => m.command))); - - command - .command('bundle') - .description('Bundle a package for deployment') - .option(...configOption) - .option('--role ', 'Run the command with an explicit package role') + .description('Build a package for production deployment or publishing') + .option( + '--minify', + 'Minify the generated code. Does not apply to app or backend packages.', + ) + .option( + '--experimental-type-build', + 'Enable experimental type build. Does not apply to app or backend packages.', + ) .option( '--skip-build-dependencies', - 'Skip the automatic building of local dependencies', + 'Skip the automatic building of local dependencies. Applies to backend packages only.', ) .option( '--stats', - 'If bundle stats are available, write them to the output directory', + 'If bundle stats are available, write them to the output directory. Applies to app packages only.', ) - .action(lazy(() => import('./bundle').then(m => m.command))); + .option( + '--config ', + 'Config files to load instead of app-config.yaml. Applies to app packages only.', + (opt: string, opts: string[]) => [...opts, opt], + Array(), + ) + .action(lazy(() => import('./build').then(m => m.command))); program .command('lint') diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts index 7a02411690..a3fdd20018 100644 --- a/packages/cli/src/commands/migrate/packageScripts.ts +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -37,9 +37,7 @@ export async function command() { const expectedScripts = { ...(hasStart && { start: 'backstage-cli script start' }), - ...(isBundled - ? { bundle: 'backstage-cli script bundle', build: undefined } - : { build: 'backstage-cli script build', bundle: undefined }), + build: 'backstage-cli script build', lint: 'backstage-cli script lint', test: 'backstage-cli script test', clean: 'backstage-cli script clean', From 680e7c7452ae6bc3b787095462f915befaf8c728 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 28 Jan 2022 15:52:08 -0700 Subject: [PATCH 26/35] Respect queryParameter updates in pickers Signed-off-by: Tim Hansen --- .changeset/seven-apes-shave.md | 5 +++ .../EntityLifecyclePicker.tsx | 16 +++++---- .../EntityOwnerPicker/EntityOwnerPicker.tsx | 14 +++++--- .../EntityTagPicker/EntityTagPicker.tsx | 16 +++++---- .../UserListPicker/UserListPicker.tsx | 11 +++++-- .../src/hooks/useEntityListProvider.tsx | 33 +++++++++++-------- .../src/hooks/useEntityTypeFilter.tsx | 14 +++++--- 7 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 .changeset/seven-apes-shave.md diff --git a/.changeset/seven-apes-shave.md b/.changeset/seven-apes-shave.md new file mode 100644 index 0000000000..356fd377dd --- /dev/null +++ b/.changeset/seven-apes-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Updated `useEntityListProvider` and catalog pickers to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters. diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index 630b1700a0..e4e42eb7ba 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -51,15 +51,19 @@ export const EntityLifecyclePicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamLifecycles = [queryParameters.lifecycles] - .flat() - .filter(Boolean) as string[]; const [selectedLifecycles, setSelectedLifecycles] = useState( - queryParamLifecycles.length - ? queryParamLifecycles - : filters.lifecycles?.values ?? [], + filters.lifecycles?.values ?? [], ); + // Set selected lifecycles on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamLifecycles = [queryParameters.lifecycles] + .flat() + .filter(Boolean) as string[]; + setSelectedLifecycles(queryParamLifecycles); + }, [queryParameters]); + useEffect(() => { updateFilters({ lifecycles: selectedLifecycles.length diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index ae88858666..95e09ed434 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -53,13 +53,19 @@ export const EntityOwnerPicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamOwners = [queryParameters.owners] - .flat() - .filter(Boolean) as string[]; const [selectedOwners, setSelectedOwners] = useState( - queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [], + filters.owners?.values ?? [], ); + // Set selected owners on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamOwners = [queryParameters.owners] + .flat() + .filter(Boolean) as string[]; + setSelectedOwners(queryParamOwners); + }, [queryParameters]); + useEffect(() => { updateFilters({ owners: selectedOwners.length diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index a25c50c0a9..9753ae14f4 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -51,12 +51,16 @@ export const EntityTagPicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamTags = [queryParameters.tags] - .flat() - .filter(Boolean) as string[]; - const [selectedTags, setSelectedTags] = useState( - queryParamTags.length ? queryParamTags : filters.tags?.values ?? [], - ); + const [selectedTags, setSelectedTags] = useState(filters.tags?.values ?? []); + + // Set selected tags on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamTags = [queryParameters.tags] + .flat() + .filter(Boolean) as string[]; + setSelectedTags(queryParamTags); + }, [queryParameters]); useEffect(() => { updateFilters({ diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index d731a185fc..4820a4f0f9 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -161,9 +161,7 @@ export const UserListPicker = ({ [isOwnedEntity, isStarredEntity], ); - const [selectedUserFilter, setSelectedUserFilter] = useState( - [queryParameters.user].flat()[0] ?? initialFilter, - ); + const [selectedUserFilter, setSelectedUserFilter] = useState(initialFilter); // To show proper counts for each section, apply all other frontend filters _except_ the user // filter that's controlled by this picker. @@ -190,6 +188,13 @@ export const UserListPicker = ({ [entitiesWithoutUserFilter, starredFilter, ownedFilter], ); + // Set selected user filter on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamUserFilter = [queryParameters.user].flat()[0]; + setSelectedUserFilter(queryParamUserFilter as UserListFilterKind); + }, [queryParameters]); + useEffect(() => { if ( !loading && diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index fb8eb7279f..9b4d850208 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -25,6 +25,7 @@ import React, { useMemo, useState, } from 'react'; +import { useLocation } from 'react-router'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import useDebounce from 'react-use/lib/useDebounce'; import useMountedState from 'react-use/lib/useMountedState'; @@ -98,7 +99,6 @@ type OutputState = { appliedFilters: EntityFilters; entities: Entity[]; backendEntities: Entity[]; - queryParameters: Record; }; export const EntityListProvider = ({ @@ -109,19 +109,26 @@ export const EntityListProvider = ({ const [requestedFilters, setRequestedFilters] = useState( {} as EntityFilters, ); + + // We use react-router's useLocation hook so updates from external sources trigger an update to + // the queryParameters in outputState. Updates from this hook use replaceState below and won't + // trigger a useLocation change; this would instead come from an external source, such as a manual + // update of the URL or two catalog sidebar links with different catalog filters. + const location = useLocation(); + const queryParameters = useMemo( + () => + (qs.parse(location.search, { + ignoreQueryPrefix: true, + }).filters ?? {}) as Record, + [location], + ); + const [outputState, setOutputState] = useState>( () => { - const query = qs.parse(window.location.search, { - ignoreQueryPrefix: true, - }); return { appliedFilters: {} as EntityFilters, entities: [], backendEntities: [], - queryParameters: (query.filters ?? {}) as Record< - string, - string | string[] - >, }; }, ); @@ -163,19 +170,17 @@ export const EntityListProvider = ({ appliedFilters: requestedFilters, backendEntities: response.items, entities: response.items.filter(entityFilter), - queryParameters: queryParams, }); } else { setOutputState({ appliedFilters: requestedFilters, backendEntities: outputState.backendEntities, entities: outputState.backendEntities.filter(entityFilter), - queryParameters: queryParams, }); } if (isMounted()) { - const oldParams = qs.parse(window.location.search, { + const oldParams = qs.parse(location.search, { ignoreQueryPrefix: true, }); const newParams = qs.stringify( @@ -191,7 +196,7 @@ export const EntityListProvider = ({ window.history?.replaceState(null, document.title, newUrl); } }, - [catalogApi, requestedFilters, outputState], + [catalogApi, queryParameters, requestedFilters, outputState], { loading: true }, ); @@ -220,11 +225,11 @@ export const EntityListProvider = ({ entities: outputState.entities, backendEntities: outputState.backendEntities, updateFilters, - queryParameters: outputState.queryParameters, + queryParameters, loading, error, }), - [outputState, updateFilters, loading, error], + [outputState, updateFilters, queryParameters, loading, error], ); return ( diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index 5c0139dd26..cbc9d4ed45 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -42,13 +42,19 @@ export function useEntityTypeFilter(): EntityTypeReturn { updateFilters, } = useEntityListProvider(); - const queryParamTypes = [queryParameters.type] - .flat() - .filter(Boolean) as string[]; const [selectedTypes, setSelectedTypes] = useState( - queryParamTypes.length ? queryParamTypes : typeFilter?.getTypes() ?? [], + typeFilter?.getTypes() ?? [], ); + // Set selected types on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + const queryParamTypes = [queryParameters.type] + .flat() + .filter(Boolean) as string[]; + setSelectedTypes(queryParamTypes); + }, [queryParameters]); + const [availableTypes, setAvailableTypes] = useState([]); const kind = useMemo(() => kindFilter?.value, [kindFilter]); From 911f01ace35f3372269fcb3fa959cf6d86c2f782 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 4 Feb 2022 12:47:18 -0700 Subject: [PATCH 27/35] Set initial value from queryParameters Add tests for external queryParameter updates Signed-off-by: Tim Hansen --- .../EntityLifecyclePicker.test.tsx | 30 +++++++++++++ .../EntityLifecyclePicker.tsx | 18 +++++--- .../EntityOwnerPicker.test.tsx | 30 +++++++++++++ .../EntityOwnerPicker/EntityOwnerPicker.tsx | 16 ++++--- .../EntityTagPicker/EntityTagPicker.test.tsx | 30 +++++++++++++ .../EntityTagPicker/EntityTagPicker.tsx | 18 +++++--- .../EntityTypePicker.test.tsx | 34 ++++++++++++++ .../UserListPicker/UserListPicker.test.tsx | 36 +++++++++++++++ .../UserListPicker/UserListPicker.tsx | 16 +++++-- .../src/hooks/useEntityTypeFilter.tsx | 16 ++++--- .../catalog-react/src/testUtils/providers.tsx | 44 ++++++++++++------- 11 files changed, 245 insertions(+), 43 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx index f7d915ef5b..47e85292e2 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx @@ -161,4 +161,34 @@ describe('', () => { lifecycles: undefined, }); }); + + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + lifecycles: new EntityLifecycleFilter(['experimental']), + }); + rendered.rerender( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + lifecycles: new EntityLifecycleFilter(['production']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index e4e42eb7ba..be0351ff97 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -51,18 +51,24 @@ export const EntityLifecyclePicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); + const queryParamLifecycles = useMemo( + () => [queryParameters.lifecycles].flat().filter(Boolean) as string[], + [queryParameters], + ); + const [selectedLifecycles, setSelectedLifecycles] = useState( - filters.lifecycles?.values ?? [], + queryParamLifecycles.length + ? queryParamLifecycles + : filters.lifecycles?.values ?? [], ); // Set selected lifecycles on query parameter updates; this happens at initial page load and from // external updates to the page location. useEffect(() => { - const queryParamLifecycles = [queryParameters.lifecycles] - .flat() - .filter(Boolean) as string[]; - setSelectedLifecycles(queryParamLifecycles); - }, [queryParameters]); + if (queryParamLifecycles.length) { + setSelectedLifecycles(queryParamLifecycles); + } + }, [queryParamLifecycles]); useEffect(() => { updateFilters({ diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index 277a691a84..14abc358db 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -191,4 +191,34 @@ describe('', () => { owner: undefined, }); }); + + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + owners: new EntityOwnerFilter(['team-a']), + }); + rendered.rerender( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + owners: new EntityOwnerFilter(['team-b']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 95e09ed434..d2a740d850 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -53,18 +53,22 @@ export const EntityOwnerPicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); + const queryParamOwners = useMemo( + () => [queryParameters.owners].flat().filter(Boolean) as string[], + [queryParameters], + ); + const [selectedOwners, setSelectedOwners] = useState( - filters.owners?.values ?? [], + queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [], ); // Set selected owners on query parameter updates; this happens at initial page load and from // external updates to the page location. useEffect(() => { - const queryParamOwners = [queryParameters.owners] - .flat() - .filter(Boolean) as string[]; - setSelectedOwners(queryParamOwners); - }, [queryParameters]); + if (queryParamOwners.length) { + setSelectedOwners(queryParamOwners); + } + }, [queryParamOwners]); useEffect(() => { updateFilters({ diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index 770c187b4e..c6b985bc8b 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -149,4 +149,34 @@ describe('', () => { tags: undefined, }); }); + + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag1']), + }); + rendered.rerender( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag2']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index 9753ae14f4..4cd17b3070 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -51,16 +51,22 @@ export const EntityTagPicker = () => { const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const [selectedTags, setSelectedTags] = useState(filters.tags?.values ?? []); + const queryParamTags = useMemo( + () => [queryParameters.tags].flat().filter(Boolean) as string[], + [queryParameters], + ); + + const [selectedTags, setSelectedTags] = useState( + queryParamTags.length ? queryParamTags : filters.tags?.values ?? [], + ); // Set selected tags on query parameter updates; this happens at initial page load and from // external updates to the page location. useEffect(() => { - const queryParamTags = [queryParameters.tags] - .flat() - .filter(Boolean) as string[]; - setSelectedTags(queryParamTags); - }, [queryParameters]); + if (queryParamTags.length) { + setSelectedTags(queryParamTags); + } + }, [queryParamTags]); useEffect(() => { updateFilters({ diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index f6e9d296df..9ed27bbef8 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -151,4 +151,38 @@ describe('', () => { type: new EntityTypeFilter(['tool']), }); }); + + it('responds to external queryParameters changes', async () => { + const updateFilters = jest.fn(); + const rendered = await renderWithEffects( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + type: new EntityTypeFilter(['service']), + }); + rendered.rerender( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + type: new EntityTypeFilter(['tool']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index b776f26926..d4cfedecbf 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -253,6 +253,42 @@ describe('', () => { }); }); + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter('all', mockIsOwnedEntity, mockIsStarredEntity), + }); + rendered.rerender( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter('owned', mockIsOwnedEntity, mockIsStarredEntity), + }); + }); + describe.each` type | filterFn ${'owned'} | ${mockIsOwnedEntity} diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 4820a4f0f9..cbf6089f2e 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -161,7 +161,14 @@ export const UserListPicker = ({ [isOwnedEntity, isStarredEntity], ); - const [selectedUserFilter, setSelectedUserFilter] = useState(initialFilter); + const queryParamUserFilter = useMemo( + () => [queryParameters.user].flat()[0], + [queryParameters], + ); + + const [selectedUserFilter, setSelectedUserFilter] = useState( + queryParamUserFilter ?? initialFilter, + ); // To show proper counts for each section, apply all other frontend filters _except_ the user // filter that's controlled by this picker. @@ -191,9 +198,10 @@ export const UserListPicker = ({ // Set selected user filter on query parameter updates; this happens at initial page load and from // external updates to the page location. useEffect(() => { - const queryParamUserFilter = [queryParameters.user].flat()[0]; - setSelectedUserFilter(queryParamUserFilter as UserListFilterKind); - }, [queryParameters]); + if (queryParamUserFilter) { + setSelectedUserFilter(queryParamUserFilter as UserListFilterKind); + } + }, [queryParamUserFilter]); useEffect(() => { if ( diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index cbc9d4ed45..c4531d9117 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -42,18 +42,22 @@ export function useEntityTypeFilter(): EntityTypeReturn { updateFilters, } = useEntityListProvider(); + const queryParamTypes = useMemo( + () => [queryParameters.type].flat().filter(Boolean) as string[], + [queryParameters], + ); + const [selectedTypes, setSelectedTypes] = useState( - typeFilter?.getTypes() ?? [], + queryParamTypes.length ? queryParamTypes : typeFilter?.getTypes() ?? [], ); // Set selected types on query parameter updates; this happens at initial page load and from // external updates to the page location. useEffect(() => { - const queryParamTypes = [queryParameters.type] - .flat() - .filter(Boolean) as string[]; - setSelectedTypes(queryParamTypes); - }, [queryParameters]); + if (queryParamTypes.length) { + setSelectedTypes(queryParamTypes); + } + }, [queryParamTypes]); const [availableTypes, setAvailableTypes] = useState([]); const kind = useMemo(() => kindFilter?.value, [kindFilter]); diff --git a/plugins/catalog-react/src/testUtils/providers.tsx b/plugins/catalog-react/src/testUtils/providers.tsx index 2172f6c5a9..5b2eae04a5 100644 --- a/plugins/catalog-react/src/testUtils/providers.tsx +++ b/plugins/catalog-react/src/testUtils/providers.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import React, { PropsWithChildren, useCallback, useState } from 'react'; +import React, { + PropsWithChildren, + useCallback, + useMemo, + useState, +} from 'react'; import { DefaultEntityFilters, EntityListContext, @@ -32,6 +37,7 @@ export const MockEntityListContextProvider = ({ const [filters, setFilters] = useState( value?.filters ?? {}, ); + const updateFilters = useCallback( ( update: @@ -49,23 +55,31 @@ export const MockEntityListContextProvider = ({ [], ); - const defaultContext: EntityListContextProps = { - entities: [], - backendEntities: [], - updateFilters, - filters, - loading: false, - queryParameters: {}, - }; + // Memoize the default values since pickers have useEffect triggers on these; naively defaulting + // below with `?? ` breaks referential equality on subsequent updates. + const defaultValues = useMemo( + () => ({ + entities: [], + backendEntities: [], + queryParameters: {}, + }), + [], + ); - // Extract value.filters to avoid overwriting it; some tests exercise filter updates. The value - // provided is used as the initial seed in useState above. - const { filters: _, ...otherContextFields } = value ?? {}; + const resolvedValue: EntityListContextProps = useMemo( + () => ({ + entities: value?.entities ?? defaultValues.entities, + backendEntities: value?.backendEntities ?? defaultValues.backendEntities, + updateFilters: value?.updateFilters ?? updateFilters, + filters, + loading: value?.loading ?? false, + queryParameters: value?.queryParameters ?? defaultValues.queryParameters, + }), + [value, defaultValues, filters, updateFilters], + ); return ( - + {children} ); From fb39efa3204c06080ac99c83084c60a9c0542cdf Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 4 Feb 2022 13:25:51 -0700 Subject: [PATCH 28/35] Fix useEntityListProvider test Signed-off-by: Tim Hansen --- .../src/hooks/useEntityListProvider.test.tsx | 50 +++++++++++-------- .../catalog-react/src/testUtils/providers.tsx | 1 + 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index a1e722bc6d..548561108d 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -27,6 +27,7 @@ import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; +import { MemoryRouter } from 'react-router'; import { catalogApiRef } from '../api'; import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; import { EntityKindPicker, UserListPicker } from '../components'; @@ -82,31 +83,35 @@ const mockCatalogApi: Partial = { const wrapper = ({ userFilter, + location, children, }: PropsWithChildren<{ userFilter?: UserListFilterKind; + location?: string; }>) => { return ( - - - - + + + + + + ); }; @@ -168,10 +173,11 @@ describe('', () => { const query = qs.stringify({ filters: { kind: 'component', type: 'service' }, }); - delete (window as any).location; - (window as any).location = new URL(`http://localhost/catalog?${query}`); const { result, waitFor } = renderHook(() => useEntityListProvider(), { wrapper, + initialProps: { + location: `/catalog?${query}`, + }, }); await waitFor(() => !!result.current.queryParameters); expect(result.current.queryParameters).toEqual({ diff --git a/plugins/catalog-react/src/testUtils/providers.tsx b/plugins/catalog-react/src/testUtils/providers.tsx index 5b2eae04a5..3fb295298c 100644 --- a/plugins/catalog-react/src/testUtils/providers.tsx +++ b/plugins/catalog-react/src/testUtils/providers.tsx @@ -74,6 +74,7 @@ export const MockEntityListContextProvider = ({ filters, loading: value?.loading ?? false, queryParameters: value?.queryParameters ?? defaultValues.queryParameters, + error: value?.error, }), [value, defaultValues, filters, updateFilters], ); From 63181dee791f857fa0c9930cd9d783655ec63103 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Feb 2022 10:42:38 +0100 Subject: [PATCH 29/35] cli: wrap minified code in iife Signed-off-by: Patrik Oldsberg --- .changeset/neat-icons-fry.md | 5 +++++ packages/cli/src/lib/bundler/optimization.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/neat-icons-fry.md diff --git a/.changeset/neat-icons-fry.md b/.changeset/neat-icons-fry.md new file mode 100644 index 0000000000..33c4a4512c --- /dev/null +++ b/.changeset/neat-icons-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Tweaked frontend bundling configuration to avoid leaking declarations into global scope. diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index a5cf38fc11..0a5ef178cb 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -29,6 +29,7 @@ export const optimization = ( minimizer: [ new ESBuildMinifyPlugin({ target: 'es2019', + format: 'iife', }), ], runtimeChunk: 'single', From d62bdb7a8e444d7a250e14b6b3c6cbe6882f2c45 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Feb 2022 14:02:16 +0100 Subject: [PATCH 30/35] core-components: make useSupportConfig fall back to default config when needed Signed-off-by: Patrik Oldsberg --- .changeset/three-pigs-sniff.md | 5 +++++ packages/core-components/src/hooks/useSupportConfig.ts | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 .changeset/three-pigs-sniff.md diff --git a/.changeset/three-pigs-sniff.md b/.changeset/three-pigs-sniff.md new file mode 100644 index 0000000000..c2020c9e80 --- /dev/null +++ b/.changeset/three-pigs-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +The `ErrorPage` now falls back to using the default support configuration if the `ConfigApi` is not available. diff --git a/packages/core-components/src/hooks/useSupportConfig.ts b/packages/core-components/src/hooks/useSupportConfig.ts index 5d64e38f3d..546d779e01 100644 --- a/packages/core-components/src/hooks/useSupportConfig.ts +++ b/packages/core-components/src/hooks/useSupportConfig.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useApi, configApiRef } from '@backstage/core-plugin-api'; +import { useApiHolder, configApiRef } from '@backstage/core-plugin-api'; export type SupportItemLink = { url: string; @@ -50,8 +50,9 @@ const DEFAULT_SUPPORT_CONFIG: SupportConfig = { }; export function useSupportConfig(): SupportConfig { - const config = useApi(configApiRef); - const supportConfig = config.getOptionalConfig('app.support'); + const apiHolder = useApiHolder(); + const config = apiHolder.get(configApiRef); + const supportConfig = config?.getOptionalConfig('app.support'); if (!supportConfig) { return DEFAULT_SUPPORT_CONFIG; From 112d2e1f0252a04cb08f44403f8d241a20ef27fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 04:19:46 +0000 Subject: [PATCH 31/35] chore(deps-dev): bump lint-staged from 12.2.2 to 12.3.3 Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.2.2 to 12.3.3. - [Release notes](https://github.com/okonet/lint-staged/releases) - [Commits](https://github.com/okonet/lint-staged/compare/v12.2.2...v12.3.3) --- updated-dependencies: - dependency-name: lint-staged dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4083a1a58e..c6cb0c4cea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15825,9 +15825,9 @@ linkify-it@^3.0.1: uc.micro "^1.0.1" lint-staged@^12.2.0: - version "12.2.2" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.2.2.tgz#e03d93b41092316e0f38b37c9630da807aae3cca" - integrity sha512-bcHEoM1M/f+K1BYdHcEuIn8K+zMOSJR3mkny6PAuQiTgcSUcRbUWaUD6porAYypxF4k1vYZZ2HutZt1p94Z1jQ== + version "12.3.3" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.3.3.tgz#0a465962fe53baa2b4b9da50801ead49a910e03b" + integrity sha512-OqcLsqcPOqzvsfkxjeBpZylgJ3SRG1RYqc9LxC6tkt6tNsq1bNVkAixBwX09f6CobcHswzqVOCBpFR1Fck0+ag== dependencies: cli-truncate "^3.1.0" colorette "^2.0.16" @@ -15835,10 +15835,10 @@ lint-staged@^12.2.0: debug "^4.3.3" execa "^5.1.1" lilconfig "2.0.4" - listr2 "^3.13.5" + listr2 "^4.0.1" micromatch "^4.0.4" normalize-path "^3.0.0" - object-inspect "^1.11.1" + object-inspect "^1.12.0" string-argv "^0.3.1" supports-color "^9.2.1" yaml "^1.10.2" @@ -15877,17 +15877,17 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr2@^3.13.5: - version "3.14.0" - resolved "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" - integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== +listr2@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/listr2/-/listr2-4.0.2.tgz#04d66f8c8694a14920d7df08ebe01568948fb500" + integrity sha512-YcgwfCWpvPbj9FLUGqvdFvd3hrFWKpOeuXznRgfWEJ7RNr8b/IKKIKZABHx3aU+4CWN/iSAFFSReziQG6vTeIA== dependencies: cli-truncate "^2.1.0" colorette "^2.0.16" log-update "^4.0.0" p-map "^4.0.0" rfdc "^1.3.0" - rxjs "^7.5.1" + rxjs "^7.5.2" through "^2.3.8" wrap-ansi "^7.0.0" @@ -18149,7 +18149,7 @@ object-hash@^2.0.1, object-hash@^2.1.1, object-hash@^2.2.0: resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -object-inspect@^1.11.0, object-inspect@^1.11.1, object-inspect@^1.9.0: +object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== @@ -21338,7 +21338,7 @@ rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.1: +rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.2: version "7.5.2" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.5.2.tgz#11e4a3a1dfad85dbf7fb6e33cbba17668497490b" integrity sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w== From 599f3dfa835d8b6f8b94ac61a960179718136d30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 04:23:29 +0000 Subject: [PATCH 32/35] chore(deps-dev): bump @types/concat-stream from 1.6.1 to 2.0.0 Bumps [@types/concat-stream](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/concat-stream) from 1.6.1 to 2.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/concat-stream) --- updated-dependencies: - dependency-name: "@types/concat-stream" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .changeset/dependabot-f436b5b.md | 5 +++++ packages/backend-common/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/dependabot-f436b5b.md diff --git a/.changeset/dependabot-f436b5b.md b/.changeset/dependabot-f436b5b.md new file mode 100644 index 0000000000..9dc9fbccc8 --- /dev/null +++ b/.changeset/dependabot-f436b5b.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +chore(deps-dev): bump `@types/concat-stream` from 1.6.1 to 2.0.0 diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index fdff7ca414..b661241623 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -88,7 +88,7 @@ "@backstage/test-utils": "^0.2.4", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", - "@types/concat-stream": "^1.6.0", + "@types/concat-stream": "^2.0.0", "@types/fs-extra": "^9.0.3", "@types/http-errors": "^1.6.3", "@types/minimist": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index 4083a1a58e..ee01ada5f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5147,10 +5147,10 @@ dependencies: "@types/express" "*" -"@types/concat-stream@^1.6.0": - version "1.6.1" - resolved "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz#24bcfc101ecf68e886aaedce60dfd74b632a1b74" - integrity sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA== +"@types/concat-stream@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-2.0.0.tgz#a716f0ba9015014e643addb351da05a73bef425c" + integrity sha512-t3YCerNM7NTVjLuICZo5gYAXYoDvpuuTceCcFQWcDQz26kxUR5uIWolxbIR5jRNIXpMqhOpW/b8imCR1LEmuJw== dependencies: "@types/node" "*" From fe0e05ddcf25d457980edd79da7c36c582907b8f Mon Sep 17 00:00:00 2001 From: Lyupcho Kotev Date: Mon, 7 Feb 2022 09:48:59 +0100 Subject: [PATCH 33/35] Add IKEA IT AB to adopters list Signed-off-by: Lyupcho Kotev --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 39007ab1c1..120c828c43 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -89,3 +89,4 @@ | [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. | [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal | | [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality | +| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. | From 0c7936af831d45f220a061774d7f3416f81b7a0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 09:46:30 +0000 Subject: [PATCH 34/35] chore(deps-dev): bump husky from 6.0.0 to 7.0.4 Bumps [husky](https://github.com/typicode/husky) from 6.0.0 to 7.0.4. - [Release notes](https://github.com/typicode/husky/releases) - [Commits](https://github.com/typicode/husky/compare/v6.0.0...v7.0.4) --- updated-dependencies: - dependency-name: husky dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5c60cc41c6..54841128a9 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "concurrently": "^7.0.0", "eslint-plugin-notice": "^0.9.10", "fs-extra": "9.1.0", - "husky": "^6.0.0", + "husky": "^7.0.4", "lerna": "^4.0.0", "lint-staged": "^12.2.0", "minimist": "^1.2.5", diff --git a/yarn.lock b/yarn.lock index 0c8c87200b..386ba1955c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13483,10 +13483,10 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" - integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== +husky@^7.0.4: + version "7.0.4" + resolved "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" + integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: version "1.0.3" From a4a777441de6f1619e19bcefc7b958ee127335d2 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Mon, 7 Feb 2022 11:28:26 +0100 Subject: [PATCH 35/35] [Home] homepage component starred entities (#9378) * implement starred entities home page component + add to storybook Signed-off-by: Emma Indal * update default template to include starred entities Signed-off-by: Emma Indal * add changeset Signed-off-by: Emma Indal * api report Signed-off-by: Emma Indal --- .changeset/wise-plants-tease.md | 5 ++ plugins/home/api-report.md | 7 ++ plugins/home/package.json | 4 +- .../StarredEntities/Content.test.tsx | 70 +++++++++++++++++ .../StarredEntities/Content.tsx | 75 +++++++++++++++++++ .../StarredEntities.stories.tsx | 73 ++++++++++++++++++ .../StarredEntities/index.ts | 17 +++++ plugins/home/src/index.ts | 1 + plugins/home/src/plugin.ts | 13 ++++ .../src/templates/DefaultTemplate.stories.tsx | 60 +++++++++++---- 10 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 .changeset/wise-plants-tease.md create mode 100644 plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx create mode 100644 plugins/home/src/homePageComponents/StarredEntities/Content.tsx create mode 100644 plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx create mode 100644 plugins/home/src/homePageComponents/StarredEntities/index.ts diff --git a/.changeset/wise-plants-tease.md b/.changeset/wise-plants-tease.md new file mode 100644 index 0000000000..206704435d --- /dev/null +++ b/.changeset/wise-plants-tease.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Adds new StarredEntities component responsible for rendering a list of starred entities on the home page diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index fab2b05bb6..264189ee07 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -113,6 +113,13 @@ export const HomePageRandomJoke: ( }, ) => JSX.Element; +// @public +export const HomePageStarredEntities: ( + props: ComponentRenderer & { + title?: string | undefined; + }, +) => JSX.Element; + // Warning: (ae-forgotten-export) The symbol "ToolkitContentProps" needs to be exported by the entry point index.d.ts // // @public diff --git a/plugins/home/package.json b/plugins/home/package.json index a1a09329f8..b1d4fc362e 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -21,10 +21,12 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.9.10", "@backstage/core-components": "^0.8.7", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/theme": "^0.2.14", + "@backstage/plugin-catalog-react": "^0.6.13", "@backstage/plugin-search": "^0.6.1", + "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx new file mode 100644 index 0000000000..ccc36f6964 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2022 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 { + renderInTestApp, + TestApiProvider, + MockStorageApi, +} from '@backstage/test-utils'; +import { + starredEntitiesApiRef, + entityRouteRef, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { Content } from './Content'; + +describe('StarredEntitiesContent', () => { + it('should render list of tools', async () => { + const mockStorageApi = MockStorageApi.create(); + await mockStorageApi + .forBucket('starredEntities') + .set('entityRefs', [ + 'component:default/mock-starred-entity', + 'component:default/mock-starred-entity-2', + ]); + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByText('mock-starred-entity')).toBeInTheDocument(); + expect(getByText('mock-starred-entity-2')).toBeInTheDocument(); + expect(getByText('mock-starred-entity').closest('a')).toHaveAttribute( + 'href', + '/catalog/default/component/mock-starred-entity', + ); + expect(getByText('mock-starred-entity-2').closest('a')).toHaveAttribute( + 'href', + '/catalog/default/component/mock-starred-entity-2', + ); + }); +}); diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx new file mode 100644 index 0000000000..6c9775c842 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2022 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 { + useStarredEntities, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { Link } from '@backstage/core-components'; +import { + List, + ListItem, + ListItemSecondaryAction, + IconButton, + ListItemText, + Tooltip, + Typography, +} from '@material-ui/core'; +import StarIcon from '@material-ui/icons/Star'; +import React from 'react'; + +/** + * A component to display a list of starred entities for the user. + * + * @public + */ + +export const Content = () => { + const catalogEntityRoute = useRouteRef(entityRouteRef); + const { starredEntities, toggleStarredEntity } = useStarredEntities(); + + if (starredEntities.size === 0) + return ( + + You do not have any starred entities yet! + + ); + + return ( + + {Array.from(starredEntities).map(entity => ( + + + + + + + toggleStarredEntity(entity)} + > + + + + + + ))} + + ); +}; diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx new file mode 100644 index 0000000000..2e63762369 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2022 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 { HomePageStarredEntities } from '../../plugin'; +import { + wrapInTestApp, + TestApiProvider, + MockStorageApi, +} from '@backstage/test-utils'; +import { + starredEntitiesApiRef, + entityRouteRef, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; +import { Grid } from '@material-ui/core'; +import React, { ComponentType } from 'react'; + +const mockStorageApi = MockStorageApi.create(); +mockStorageApi + .forBucket('starredEntities') + .set('entityRefs', [ + 'component:default/example-starred-entity', + 'component:default/example-starred-entity-2', + 'component:default/example-starred-entity-3', + 'component:default/example-starred-entity-4', + ]); + +export default { + title: 'Plugins/Home/Components/StarredEntities', + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ], +}; + +export const Default = () => { + return ( + + + + ); +}; diff --git a/plugins/home/src/homePageComponents/StarredEntities/index.ts b/plugins/home/src/homePageComponents/StarredEntities/index.ts new file mode 100644 index 0000000000..1faa9a2426 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { Content } from './Content'; diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts index dcd61f1fa3..4871323677 100644 --- a/plugins/home/src/index.ts +++ b/plugins/home/src/index.ts @@ -26,6 +26,7 @@ export { HomePageRandomJoke, HomePageToolkit, HomePageCompanyLogo, + HomePageStarredEntities, ComponentAccordion, ComponentTabs, ComponentTab, diff --git a/plugins/home/src/plugin.ts b/plugins/home/src/plugin.ts index 135ac97af0..32005a9064 100644 --- a/plugins/home/src/plugin.ts +++ b/plugins/home/src/plugin.ts @@ -115,3 +115,16 @@ export const HomePageToolkit = homePlugin.provide( components: () => import('./homePageComponents/Toolkit'), }), ); + +/** + * A component to display a list of starred entities for the user. + * + * @public + */ +export const HomePageStarredEntities = homePlugin.provide( + createCardExtension({ + name: 'HomePageStarredEntities', + title: 'Your Starred Entities', + components: () => import('./homePageComponents/StarredEntities'), + }), +); diff --git a/plugins/home/src/templates/DefaultTemplate.stories.tsx b/plugins/home/src/templates/DefaultTemplate.stories.tsx index fb62591fbd..ba5e1f33fd 100644 --- a/plugins/home/src/templates/DefaultTemplate.stories.tsx +++ b/plugins/home/src/templates/DefaultTemplate.stories.tsx @@ -14,20 +14,38 @@ * limitations under the License. */ -import {TemplateBackstageLogo} from './TemplateBackstageLogo'; -import {TemplateBackstageLogoIcon} from './TemplateBackstageLogoIcon'; -import { HomePageToolkit, HomePageCompanyLogo } from '../plugin'; -import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { TemplateBackstageLogo } from './TemplateBackstageLogo'; +import { TemplateBackstageLogoIcon } from './TemplateBackstageLogoIcon'; +import { + HomePageToolkit, + HomePageCompanyLogo, + HomePageStarredEntities, +} from '../plugin'; +import { wrapInTestApp, TestApiProvider, MockStorageApi} from '@backstage/test-utils'; import { Content, Page, InfoCard } from '@backstage/core-components'; +import { + starredEntitiesApiRef, + entityRouteRef, + DefaultStarredEntitiesApi +} from '@backstage/plugin-catalog-react'; import { HomePageSearchBar, SearchContextProvider, searchApiRef, - searchPlugin + searchPlugin, } from '@backstage/plugin-search'; import { Grid, makeStyles } from '@material-ui/core'; -import React, { ComponentType} from 'react'; +import React, { ComponentType } from 'react'; +const mockStorageApi = MockStorageApi.create(); +mockStorageApi + .forBucket('starredEntities') + .set('entityRefs', [ + 'component:default/example-starred-entity', + 'component:default/example-starred-entity-2', + 'component:default/example-starred-entity-3', + 'component:default/example-starred-entity-4' + ]); export default { title: 'Plugins/Home/Templates', @@ -35,13 +53,26 @@ export default { (Story: ComponentType<{}>) => wrapInTestApp( <> - Promise.resolve({results: []})}]]}> - + Promise.resolve({ results: [] }) }], + ]} + > + , { - mountedRoutes: {'/hello-company': searchPlugin.routes.root } - } + mountedRoutes: { + '/hello-company': searchPlugin.routes.root, + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ), ], }; @@ -82,20 +113,17 @@ export const DefaultTemplate = () => { } + logo={} /> - - {/* placeholder for content */} -
- +