From a7d5efa4a92225d97e73bf2d7cf49c5d22c13710 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Fri, 29 Sep 2023 18:13:01 -0400 Subject: [PATCH 001/141] Fix TechDocs page scroll. Move logic from scrollIntoAnchor transformer to a useEffect . Signed-off-by: Sydney Achinger --- .../TechDocsReaderPageContent.tsx | 18 ++- .../TechDocsReaderPageContent/dom.tsx | 2 - .../techdocs/src/reader/transformers/index.ts | 1 - .../transformers/scrollIntoAnchor.test.ts | 118 ------------------ .../reader/transformers/scrollIntoAnchor.ts | 37 ------ 5 files changed, 16 insertions(+), 160 deletions(-) delete mode 100644 plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts delete mode 100644 plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx index 1e5459fc9d..ad25c5c81d 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx @@ -14,12 +14,14 @@ * limitations under the License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { makeStyles, Grid } from '@material-ui/core'; import { TechDocsShadowDom, + useShadowDomStylesLoading, + useShadowRootElements, useTechDocsReaderPage, } from '@backstage/plugin-techdocs-react'; import { CompoundEntityRef } from '@backstage/catalog-model'; @@ -78,8 +80,20 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider( entityRef, setShadowRoot, } = useTechDocsReaderPage(); - const dom = useTechDocsReaderDom(entityRef); + const hash = window.location.hash; + const isStyleLoading = useShadowDomStylesLoading(dom); + const [hashElement] = useShadowRootElements([`[id="${hash.slice(1)}"]`]); + + useEffect(() => { + if (hashElement) { + if (!isStyleLoading) { + hashElement.scrollIntoView(); + } + } else { + document?.querySelector('header')?.scrollIntoView(); + } + }, [hashElement, isStyleLoading, dom]); const handleAppend = useCallback( (newShadowRoot: ShadowRoot) => { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index cc42c46b4e..29e6945fa7 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -39,7 +39,6 @@ import { removeMkdocsHeader, rewriteDocLinks, simplifyMkdocsFooter, - scrollIntoAnchor, scrollIntoNavigation, transform as transformer, copyToClipboard, @@ -167,7 +166,6 @@ export const useTechDocsReaderDom = ( const postRender = useCallback( async (transformedElement: Element) => transformer(transformedElement, [ - scrollIntoAnchor(), scrollIntoNavigation(), copyToClipboard(theme), addLinkClickListener({ diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index ca17261572..7df98f1f26 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -25,6 +25,5 @@ export * from './copyToClipboard'; export * from './removeMkdocsHeader'; export * from './simplifyMkdocsFooter'; export * from './onCssReady'; -export * from './scrollIntoAnchor'; export * from './scrollIntoNavigation'; export * from './transformer'; diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts deleted file mode 100644 index e222c24a4c..0000000000 --- a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.test.ts +++ /dev/null @@ -1,118 +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 { scrollIntoAnchor } from '../transformers'; -import { createTestShadowDom } from '../../test-utils'; -import { Transformer } from './transformer'; -import mkdocsIndex from '../../test-utils/fixtures/mkdocs-index'; -import { SHADOW_DOM_STYLE_LOAD_EVENT } from '@backstage/plugin-techdocs-react'; - -describe('scrollIntoAnchor', () => { - const scrollIntoView = jest.fn(); - let querySelectorSpy: jest.SpyInstance; - let addEventListenerSpy: jest.SpyInstance; - let removeEventListenerSpy: jest.SpyInstance; - const applySpies: Transformer = dom => { - querySelectorSpy = jest.spyOn(dom, 'querySelector'); - querySelectorSpy.mockReturnValue({ scrollIntoView }); - addEventListenerSpy = jest.spyOn(dom, 'addEventListener'); - removeEventListenerSpy = jest.spyOn(dom, 'removeEventListener'); - return dom; - }; - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('does nothing if there is no anchor element', async () => { - await createTestShadowDom(mkdocsIndex, { - preTransformers: [], - postTransformers: [applySpies, scrollIntoAnchor()], - }); - expect(querySelectorSpy).not.toHaveBeenCalled(); - expect(addEventListenerSpy).toHaveBeenCalled(); - expect(removeEventListenerSpy).toHaveBeenCalled(); - expect(addEventListenerSpy).toHaveBeenCalledWith( - SHADOW_DOM_STYLE_LOAD_EVENT, - expect.any(Function), - ); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); - expect(removeEventListenerSpy).toHaveBeenCalledWith( - SHADOW_DOM_STYLE_LOAD_EVENT, - expect.any(Function), - ); - // check that the same function is passed to both addEventListener and removeEventListener - expect(addEventListenerSpy.mock.calls[0][1]).toBe( - removeEventListenerSpy.mock.calls[0][1], - ); - }); - - it('scroll to the hash anchor element', async () => { - window.location.hash = '#hash'; - await createTestShadowDom(mkdocsIndex, { - preTransformers: [], - postTransformers: [applySpies, scrollIntoAnchor()], - }); - expect(querySelectorSpy).toHaveBeenCalledWith( - expect.stringMatching('[id="hash"]'), - ); - expect(scrollIntoView).toHaveBeenCalledWith(); - expect(addEventListenerSpy).toHaveBeenCalledTimes(1); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); - expect(addEventListenerSpy).toHaveBeenCalledWith( - SHADOW_DOM_STYLE_LOAD_EVENT, - expect.any(Function), - ); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); - expect(removeEventListenerSpy).toHaveBeenCalledWith( - SHADOW_DOM_STYLE_LOAD_EVENT, - expect.any(Function), - ); - // check that the same function is passed to both addEventListener and removeEventListener - expect(addEventListenerSpy.mock.calls[0][1]).toBe( - removeEventListenerSpy.mock.calls[0][1], - ); - window.location.hash = ''; - }); - - it('works for anchor starting with number', async () => { - querySelectorSpy.mockReturnValue({ scrollIntoView }); - window.location.hash = '#1-hash'; - await createTestShadowDom(mkdocsIndex, { - preTransformers: [], - postTransformers: [applySpies, scrollIntoAnchor()], - }); - expect(querySelectorSpy).toHaveBeenCalledWith( - expect.stringMatching('[id="1-hash"]'), - ); - expect(scrollIntoView).toHaveBeenCalledWith(); - expect(addEventListenerSpy).toHaveBeenCalledTimes(1); - expect(addEventListenerSpy).toHaveBeenCalledWith( - SHADOW_DOM_STYLE_LOAD_EVENT, - expect.any(Function), - ); - expect(removeEventListenerSpy).toHaveBeenCalledTimes(1); - expect(removeEventListenerSpy).toHaveBeenCalledWith( - SHADOW_DOM_STYLE_LOAD_EVENT, - expect.any(Function), - ); - // check that the same function is passed to both addEventListener and removeEventListener - expect(addEventListenerSpy.mock.calls[0][1]).toBe( - removeEventListenerSpy.mock.calls[0][1], - ); - window.location.hash = ''; - }); -}); diff --git a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts b/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts deleted file mode 100644 index 97a18b7241..0000000000 --- a/plugins/techdocs/src/reader/transformers/scrollIntoAnchor.ts +++ /dev/null @@ -1,37 +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 type { Transformer } from './transformer'; -import { SHADOW_DOM_STYLE_LOAD_EVENT } from '@backstage/plugin-techdocs-react'; - -export const scrollIntoAnchor = (): Transformer => { - return dom => { - dom.addEventListener( - SHADOW_DOM_STYLE_LOAD_EVENT, - function handleShadowDomStyleLoad() { - if (window.location.hash) { - const hash = window.location.hash.slice(1); - dom?.querySelector(`[id="${hash}"]`)?.scrollIntoView(); - } - dom.removeEventListener( - SHADOW_DOM_STYLE_LOAD_EVENT, - handleShadowDomStyleLoad, - ); - }, - ); - return dom; - }; -}; From 99d4936f6c220fca7990bd40423ca6052eddf13c Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Tue, 10 Oct 2023 23:11:21 +0530 Subject: [PATCH 002/141] Add examples for github:webhook scaffolder actions Signed-off-by: parmar-abhinav --- .changeset/sweet-countries-share.md | 5 + .../github/githubWebhook.examples.test.ts | 209 ++++++++++++++++++ .../builtin/github/githubWebhook.examples.ts | 120 ++++++++++ .../actions/builtin/github/githubWebhook.ts | 2 + 4 files changed, 336 insertions(+) create mode 100644 .changeset/sweet-countries-share.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts diff --git a/.changeset/sweet-countries-share.md b/.changeset/sweet-countries-share.md new file mode 100644 index 0000000000..e1327a74b8 --- /dev/null +++ b/.changeset/sweet-countries-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `github:webhook` scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts new file mode 100644 index 0000000000..ce1fe76c53 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.test.ts @@ -0,0 +1,209 @@ +/* + * Copyright 2023 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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { createGithubWebhookAction } from './githubWebhook'; +import yaml from 'yaml'; +import { examples } from './githubWebhook.examples'; + +const mockOctokit = { + rest: { + repos: { + createWebhook: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:webhook examples', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const defaultWebhookSecret = 'aafdfdivierernfdk23f'; + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubWebhookAction({ + integrations, + defaultWebhookSecret, + githubCredentialsProvider, + }); + }); + + it('Create a GitHub webhook for a repository', async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: input.webhookUrl, + content_type: input.contentType, + secret: input.webhookSecret, + insecure_ssl: '0', + }, + events: input.events, + active: input.active, + }); + }); + + it('Create a GitHub webhook with minimal configuration', async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push'], + active: true, + }); + }); + + it('Create a GitHub webhook with custom events', async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push', 'pull_request'], + active: true, + }); + }); + + it('Create a GitHub webhook with JSON content type', async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'json', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push'], + active: true, + }); + }); + + it('Create a GitHub webhook with insecure SSL', async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '1', + }, + events: ['push'], + active: true, + }); + }); + + it('Create an inactive GitHub webhook', async () => { + const input = yaml.parse(examples[5].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect(mockOctokit.rest.repos.createWebhook).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + config: { + url: 'https://example.com/my-webhook', + content_type: 'form', + secret: defaultWebhookSecret, + insecure_ssl: '0', + }, + events: ['push'], + active: false, + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts new file mode 100644 index 0000000000..b6d4a8f80e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.examples.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Create a GitHub webhook for a repository', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + webhookSecret: 'mysecret', + events: ['push'], + active: true, + contentType: 'json', + insecureSsl: false, + token: 'my-github-token', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with minimal configuration', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with custom events', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + events: ['push', 'pull_request'], + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with JSON content type', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + contentType: 'json', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub webhook with insecure SSL', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + insecureSsl: true, + }, + }, + ], + }), + }, + { + description: 'Create an inactive GitHub webhook', + example: yaml.stringify({ + steps: [ + { + action: 'github:webhook', + name: 'Create GitHub Webhook', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + webhookUrl: 'https://example.com/my-webhook', + active: false, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts index 4d487befc7..5589c951a5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -24,6 +24,7 @@ import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { getOctokitOptions } from './helpers'; import { parseRepoUrl } from '../publish/util'; +import { examples } from './githubWebhook.examples'; /** * Creates new action that creates a webhook for a repository on GitHub. @@ -51,6 +52,7 @@ export function createGithubWebhookAction(options: { }>({ id: 'github:webhook', description: 'Creates webhook for a repository on GitHub.', + examples, schema: { input: { type: 'object', From 0920fd02ac4701b00d36ffcfbcb08447e5d0d9f6 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Wed, 11 Oct 2023 16:22:39 +0530 Subject: [PATCH 003/141] Add examples for github:environment:create scaffolder action Signed-off-by: parmar-abhinav --- .changeset/long-turkeys-argue.md | 5 + .../github/gitHubEnvironment.examples.ts | 99 +++++++ .../github/githubEnvironment.examples.test.ts | 260 ++++++++++++++++++ .../builtin/github/githubEnvironment.ts | 2 + 4 files changed, 366 insertions(+) create mode 100644 .changeset/long-turkeys-argue.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/gitHubEnvironment.examples.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.examples.test.ts diff --git a/.changeset/long-turkeys-argue.md b/.changeset/long-turkeys-argue.md new file mode 100644 index 0000000000..f051d42478 --- /dev/null +++ b/.changeset/long-turkeys-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `github:environment:create` scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/gitHubEnvironment.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/gitHubEnvironment.examples.ts new file mode 100644 index 0000000000..606424bef0 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/gitHubEnvironment.examples.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2023 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: + 'Create a GitHub Environment (No Policies, No Variables, No Secrets)', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub Environment with Protected Branch Policy', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + deploymentBranchPolicy: { + protected_branches: true, + custom_branch_policies: false, + }, + }, + }, + ], + }), + }, + { + description: 'Create a GitHub Environment with Custom Branch Policies', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + deploymentBranchPolicy: { + protected_branches: false, + custom_branch_policies: true, + }, + customBranchPolicyNames: ['main', '*.*.*'], + }, + }, + ], + }), + }, + { + description: + 'Create a GitHub Environment with Environment Variables and Secrets', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + environmentVariables: { + key1: 'val1', + key2: 'val2', + }, + secrets: { + secret1: 'supersecret1', + secret2: 'supersecret2', + }, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.examples.test.ts new file mode 100644 index 0000000000..5e3da2fcec --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.examples.test.ts @@ -0,0 +1,260 @@ +/* + * Copyright 2023 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 { PassThrough } from 'stream'; +import { createGithubEnvironmentAction } from './githubEnvironment'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import yaml from 'yaml'; +import { examples } from './gitHubEnvironment.examples'; + +const mockOctokit = { + rest: { + actions: { + getRepoPublicKey: jest.fn(), + createEnvironmentVariable: jest.fn(), + createOrUpdateEnvironmentSecret: jest.fn(), + }, + repos: { + createDeploymentBranchPolicy: jest.fn(), + createOrUpdateEnvironment: jest.fn(), + get: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +describe('github:environment:create examples', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let action: TemplateAction; + + const mockContext = { + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + id: 'repoid', + }, + }); + + action = createGithubEnvironmentAction({ + integrations, + }); + }); + + afterEach(jest.resetAllMocks); + + it('Create a GitHub Environment (No Policies, No Variables, No Secrets)', async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + expect(mockOctokit.rest.actions.getRepoPublicKey).not.toHaveBeenCalled(); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it('Create a GitHub Environment with Protected Branch Policy', async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + protected_branches: true, + custom_branch_policies: false, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + expect(mockOctokit.rest.actions.getRepoPublicKey).not.toHaveBeenCalled(); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it('Create a GitHub Environment with Custom Branch Policies', async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + protected_branches: false, + custom_branch_policies: true, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: 'main', + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: '*.*.*', + }); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it('Create a GitHub Environment with Environment Variables and Secrets', async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + name: 'key1', + value: 'val1', + }); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + name: 'key2', + value: 'val2', + }); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + secret_name: 'secret1', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + secret_name: 'secret2', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts index 04b33ed259..874798e4df 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts @@ -21,6 +21,7 @@ import { parseRepoUrl } from '../publish/util'; import { getOctokitOptions } from './helpers'; import { Octokit } from 'octokit'; import Sodium from 'libsodium-wrappers'; +import { examples } from './gitHubEnvironment.examples'; /** * Creates an `github:environment:create` Scaffolder action that creates a Github Environment. @@ -47,6 +48,7 @@ export function createGithubEnvironmentAction(options: { }>({ id: 'github:environment:create', description: 'Creates Deployment Environments', + examples, schema: { input: { type: 'object', From d0566f5184ffd7a08017cdf33b9a50855546b9f6 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Mon, 16 Oct 2023 15:52:41 -0400 Subject: [PATCH 004/141] Add mock for Element.scrollIntoView to fix tests. Signed-off-by: Sydney Achinger --- .../TechDocsReaderPage/TechDocsReaderPage.test.tsx | 2 ++ .../TechDocsReaderPageContent/TechDocsReaderPageContent.tsx | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index e11b7b6e81..c9339171e3 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -91,6 +91,8 @@ const configApi = new MockConfigApi({ }, }); +Element.prototype.scrollIntoView = jest.fn(); + const Wrapper = ({ children }: { children: React.ReactNode }) => { return ( { - if (hashElement) { - if (!isStyleLoading) { - hashElement.scrollIntoView(); - } + if (hashElement && !isStyleLoading) { + hashElement.scrollIntoView(); } else { document?.querySelector('header')?.scrollIntoView(); } From 2daffe3f7d88cfda2c5716f05cbd9f3b3105e978 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Mon, 16 Oct 2023 16:40:00 -0400 Subject: [PATCH 005/141] Add mock for Element.scrollIntoView to fix tests. Signed-off-by: Sydney Achinger --- packages/app-defaults/src/setupTests.ts | 2 ++ .../components/TechDocsReaderPage/TechDocsReaderPage.test.tsx | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-defaults/src/setupTests.ts b/packages/app-defaults/src/setupTests.ts index 963c0f188b..5f1220066f 100644 --- a/packages/app-defaults/src/setupTests.ts +++ b/packages/app-defaults/src/setupTests.ts @@ -15,3 +15,5 @@ */ import '@testing-library/jest-dom'; + +Element.prototype.scroll = jest.fn(); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index c9339171e3..e11b7b6e81 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -91,8 +91,6 @@ const configApi = new MockConfigApi({ }, }); -Element.prototype.scrollIntoView = jest.fn(); - const Wrapper = ({ children }: { children: React.ReactNode }) => { return ( Date: Mon, 16 Oct 2023 22:15:01 +0000 Subject: [PATCH 006/141] build(deps): bump undici from 5.25.4 to 5.26.3 Bumps [undici](https://github.com/nodejs/undici) from 5.25.4 to 5.26.3. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/commits/v5.26.3) --- updated-dependencies: - dependency-name: undici dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4a2d73dd4d..49c1603c65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41554,11 +41554,11 @@ __metadata: linkType: hard "undici@npm:^5.12.0, undici@npm:^5.24.0, undici@npm:^5.8.0": - version: 5.25.4 - resolution: "undici@npm:5.25.4" + version: 5.26.3 + resolution: "undici@npm:5.26.3" dependencies: "@fastify/busboy": ^2.0.0 - checksum: 654da161687de893127a685be61a19cb5bae42f4595c316ebf633929d871ac3bcd33edcb74156cea90655dfcd100bfe9b53a4f4749d52fc6ad2232f49a6ca8ab + checksum: aaa9aadb712cf80e1a9cea2377e4842670105e00abbc184a21770ea5a8b77e4e2eadc200eac62442e74a1cd3b16a840c6f73b112b9e886bd3c1a125eb22e4f21 languageName: node linkType: hard From 1db998ce6b3711df262fd32cb7a7bdc38411aa5b Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Tue, 17 Oct 2023 13:35:37 -0400 Subject: [PATCH 007/141] Fix tests Signed-off-by: Sydney Achinger --- packages/app-defaults/src/setupTests.ts | 2 -- plugins/techdocs-module-addons-contrib/src/setupTests.ts | 2 ++ plugins/techdocs/src/setupTests.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/app-defaults/src/setupTests.ts b/packages/app-defaults/src/setupTests.ts index 5f1220066f..963c0f188b 100644 --- a/packages/app-defaults/src/setupTests.ts +++ b/packages/app-defaults/src/setupTests.ts @@ -15,5 +15,3 @@ */ import '@testing-library/jest-dom'; - -Element.prototype.scroll = jest.fn(); diff --git a/plugins/techdocs-module-addons-contrib/src/setupTests.ts b/plugins/techdocs-module-addons-contrib/src/setupTests.ts index 326d2ed212..56b9b18321 100644 --- a/plugins/techdocs-module-addons-contrib/src/setupTests.ts +++ b/plugins/techdocs-module-addons-contrib/src/setupTests.ts @@ -14,3 +14,5 @@ * limitations under the License. */ import '@testing-library/jest-dom'; + +Element.prototype.scrollIntoView = jest.fn(); diff --git a/plugins/techdocs/src/setupTests.ts b/plugins/techdocs/src/setupTests.ts index 963c0f188b..6c7fc2d3e3 100644 --- a/plugins/techdocs/src/setupTests.ts +++ b/plugins/techdocs/src/setupTests.ts @@ -15,3 +15,5 @@ */ import '@testing-library/jest-dom'; + +Element.prototype.scrollIntoView = jest.fn(); From 49d3bd983325c179ea27b157cbf6cda053bf3191 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Tue, 17 Oct 2023 15:12:15 -0400 Subject: [PATCH 008/141] Remove unnecessary dep from dep array. Signed-off-by: Sydney Achinger --- .../TechDocsReaderPageContent/TechDocsReaderPageContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx index b8570ba25d..32fe788716 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx @@ -91,7 +91,7 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider( } else { document?.querySelector('header')?.scrollIntoView(); } - }, [hashElement, isStyleLoading, dom]); + }, [hash, hashElement, isStyleLoading]); const handleAppend = useCallback( (newShadowRoot: ShadowRoot) => { From 4728b3960d0dcda08ab7bafc9b74ff5fb221a515 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Tue, 17 Oct 2023 16:16:27 -0400 Subject: [PATCH 009/141] changeset Signed-off-by: Sydney Achinger --- .changeset/fresh-crews-promise.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fresh-crews-promise.md diff --git a/.changeset/fresh-crews-promise.md b/.changeset/fresh-crews-promise.md new file mode 100644 index 0000000000..fc9859086a --- /dev/null +++ b/.changeset/fresh-crews-promise.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': patch +'@backstage/plugin-techdocs': patch +--- + +Fixed navigation bug that caused users to not be scrolled to the top of a new page. Fixed navigation bug where using backwards and forwards browser navigation did not scroll users to the correct place on the TechDoc page. From ded2058de97400263dcd5e99f24e400d7ab0c5c8 Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Wed, 18 Oct 2023 09:10:42 +0200 Subject: [PATCH 010/141] scaffolder cookiecutter: remove usage of mock-fs Signed-off-by: Niklas Aronsson --- .../package.json | 3 +- .../src/actions/fetch/cookiecutter.test.ts | 28 ++++++------------- yarn.lock | 3 +- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 68bbd40294..b2d8016325 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -41,11 +41,10 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", - "@types/mock-fs": "^4.13.0", - "mock-fs": "^5.2.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index f944b6e598..3872b2e302 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -22,8 +22,7 @@ import { import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { ScmIntegrations } from '@backstage/integration'; -import mockFs from 'mock-fs'; -import os from 'os'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { PassThrough } from 'stream'; import { createFetchCookiecutterAction } from './cookiecutter'; import { join } from 'path'; @@ -47,6 +46,7 @@ jest.mock( ); describe('fetch:cookiecutter', () => { + const mockDir = createMockDirectory(); const integrations = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -58,7 +58,7 @@ describe('fetch:cookiecutter', () => { }), ); - const mockTmpDir = os.tmpdir(); + const mockTmpDir = mockDir.path; let mockContext: ActionContext<{ url: string; @@ -106,36 +106,26 @@ describe('fetch:cookiecutter', () => { output: jest.fn(), createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; - - // mock the temp directory - mockFs({ [mockTmpDir]: {} }); - mockFs({ [`${join(mockTmpDir, 'template')}`]: {} }); + mockDir.clear(); + mockDir.addContent({ template: {} }); commandExists.mockResolvedValue(null); // Mock when run container is called it creates some new files in the mock filesystem containerRunner.runContainer.mockImplementation(async () => { - mockFs({ - [`${join(mockTmpDir, 'intermediate')}`]: { - 'testfile.json': '{}', - }, + mockDir.addContent({ + 'intermediate/testfile.json': '{}', }); }); // Mock when executeShellCommand is called it creates some new files in the mock filesystem executeShellCommand.mockImplementation(async () => { - mockFs({ - [`${join(mockTmpDir, 'intermediate')}`]: { - 'testfile.json': '{}', - }, + mockDir.addContent({ + 'intermediate/testfile.json': '{}', }); }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error when copyWithoutRender is not an array', async () => { (mockContext.input as any).copyWithoutRender = 'not an array'; diff --git a/yarn.lock b/yarn.lock index 9f79ca0ec4..8fc014912f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8710,6 +8710,7 @@ __metadata: resolution: "@backstage/plugin-scaffolder-backend-module-cookiecutter@workspace:plugins/scaffolder-backend-module-cookiecutter" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -8718,10 +8719,8 @@ __metadata: "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 - "@types/mock-fs": ^4.13.0 command-exists: ^1.2.9 fs-extra: 10.1.0 - mock-fs: ^5.2.0 msw: ^1.0.0 winston: ^3.2.1 yn: ^4.0.0 From 193ad022bbb135666c11a808b31423ff682233b6 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 18 Oct 2023 13:45:26 +0100 Subject: [PATCH 011/141] add `factRetrieverId` to the logger metadata Signed-off-by: Brian Fletcher --- .changeset/plenty-tigers-argue.md | 5 +++++ .../src/service/fact/FactRetrieverEngine.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/plenty-tigers-argue.md diff --git a/.changeset/plenty-tigers-argue.md b/.changeset/plenty-tigers-argue.md new file mode 100644 index 0000000000..ef9e58d63b --- /dev/null +++ b/.changeset/plenty-tigers-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-insights-backend': patch +--- + +Add `factRetrieverId` to the fact retriever's logger metadata. diff --git a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts index 846871db88..b723fcd7f5 100644 --- a/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts +++ b/plugins/tech-insights-backend/src/service/fact/FactRetrieverEngine.ts @@ -175,6 +175,7 @@ export class DefaultFactRetrieverEngine implements FactRetrieverEngine { try { facts = await factRetriever.handler({ ...this.factRetrieverContext, + logger: this.logger.child({ factRetrieverId: factRetriever.id }), entityFilter: factRetriever.entityFilter, }); this.logger.debug( From 9ab0572217d79b1ed6f28b1fc8199c39564c5bd6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 15:51:56 +0200 Subject: [PATCH 012/141] core-app-api: add core.type markers for AppRouter and FlatRoutes Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .changeset/tidy-camels-boil.md | 5 +++++ packages/core-app-api/src/app/AppRouter.tsx | 3 +++ packages/core-app-api/src/routing/FlatRoutes.tsx | 8 +++++++- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-camels-boil.md diff --git a/.changeset/tidy-camels-boil.md b/.changeset/tidy-camels-boil.md new file mode 100644 index 0000000000..6e59f96f34 --- /dev/null +++ b/.changeset/tidy-camels-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Add component data `core.type` marker for `AppRouter` and `FlatRoutes`. diff --git a/packages/core-app-api/src/app/AppRouter.tsx b/packages/core-app-api/src/app/AppRouter.tsx index 9d07be1ffd..7481f6662a 100644 --- a/packages/core-app-api/src/app/AppRouter.tsx +++ b/packages/core-app-api/src/app/AppRouter.tsx @@ -16,6 +16,7 @@ import React, { useContext, ReactNode, ComponentType, useState } from 'react'; import { + attachComponentData, ConfigApi, configApiRef, IdentityApi, @@ -186,3 +187,5 @@ export function AppRouter(props: AppRouterProps) { ); } + +attachComponentData(AppRouter, 'core.type', 'AppRouter'); diff --git a/packages/core-app-api/src/routing/FlatRoutes.tsx b/packages/core-app-api/src/routing/FlatRoutes.tsx index abb7fe9432..46c2270d0d 100644 --- a/packages/core-app-api/src/routing/FlatRoutes.tsx +++ b/packages/core-app-api/src/routing/FlatRoutes.tsx @@ -16,7 +16,11 @@ import React, { ReactNode, useMemo } from 'react'; import { useRoutes } from 'react-router-dom'; -import { useApp, useElementFilter } from '@backstage/core-plugin-api'; +import { + attachComponentData, + useApp, + useElementFilter, +} from '@backstage/core-plugin-api'; import { isReactRouterBeta } from '../app/isReactRouterBeta'; let warned = false; @@ -115,3 +119,5 @@ export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => { return useRoutes(withNotFound); }; + +attachComponentData(FlatRoutes, 'core.type', 'FlatRoutes'); From d267d2d92b6d0d961131ad4cd74596b6092faf8d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 15:53:52 +0200 Subject: [PATCH 013/141] core-compat-api: fix plugin and extension duplication in collected routes Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../src/collectLegacyRoutes.test.tsx | 7 ++++ .../src/collectLegacyRoutes.tsx | 37 +++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index 486c373991..85fed83e8c 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -30,6 +30,7 @@ describe('collectLegacyRoutes', () => { } /> } /> } /> + } /> , ); @@ -85,6 +86,12 @@ describe('collectLegacyRoutes', () => { disabled: false, defaultConfig: { path: 'puppetdb' }, }, + { + id: 'plugin.puppetDb.page2', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'puppetdb' }, + }, { id: 'apis.plugin.puppetdb.service', attachTo: { id: 'core', input: 'apis' }, diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index 2b149e6285..8178de2876 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -62,7 +62,10 @@ Existing tasks: export function collectLegacyRoutes( flatRoutesElement: JSX.Element, ): BackstagePlugin[] { - const results = new Array(); + const createdPluginIds = new Map< + LegacyBackstagePlugin, + Extension[] + >(); React.Children.forEach( flatRoutesElement.props.children, @@ -93,13 +96,18 @@ export function collectLegacyRoutes( ); const pluginId = plugin.getId(); - const path: string = route.props.path; - const detectedExtensions = new Array>(); + const detectedExtensions = + createdPluginIds.get(plugin) ?? new Array>(); + createdPluginIds.set(plugin, detectedExtensions); + + const path: string = route.props.path; detectedExtensions.push( createPageExtension({ - id: `plugin.${pluginId}.page`, + id: `plugin.${pluginId}.page${ + detectedExtensions.length ? detectedExtensions.length + 1 : '' + }`, defaultPath: path[0] === '/' ? path.slice(1) : path, routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined, @@ -115,23 +123,20 @@ export function collectLegacyRoutes( ), }), ); + }, + ); - detectedExtensions.push( + return Array.from(createdPluginIds).map(([plugin, extensions]) => + createPlugin({ + id: plugin.getId(), + extensions: [ + ...extensions, ...Array.from(plugin.getApis()).map(factory => createApiExtension({ factory, }), ), - ); - - results.push( - createPlugin({ - id: plugin.getId(), - extensions: detectedExtensions, - }), - ); - }, + ], + }), ); - - return results; } From 5409341b53f163cec75dec2645ebd6b9de8f170a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 15:54:38 +0200 Subject: [PATCH 014/141] core-compat-api: add convertLegacyApp Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../core-compat-api/src/convertLegacyApp.ts | 138 ++++++++++++++++++ packages/core-compat-api/src/index.ts | 1 + 2 files changed, 139 insertions(+) create mode 100644 packages/core-compat-api/src/convertLegacyApp.ts diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts new file mode 100644 index 0000000000..9343744075 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2023 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, { + Children, + Fragment, + ReactElement, + ReactNode, + isValidElement, +} from 'react'; +import { + BackstagePlugin, + ExtensionOverrides, + coreExtensionData, + createExtension, + createExtensionInput, + createExtensionOverrides, +} from '@backstage/frontend-plugin-api'; +import { getComponentData } from '@backstage/core-plugin-api'; +import { collectLegacyRoutes } from './collectLegacyRoutes'; + +function selectChildren( + rootNode: ReactNode, + selector?: (element: ReactElement<{ children?: ReactNode }>) => boolean, + strictError?: string, +): Array> { + return Children.toArray(rootNode).flatMap(node => { + if (!isValidElement<{ children?: ReactNode }>(node)) { + return []; + } + + if (node.type === Fragment) { + return selectChildren(node.props.children, selector, strictError); + } + + if (selector === undefined || selector(node)) { + return [node]; + } + + if (strictError) { + throw new Error(strictError); + } + + return selectChildren(node.props.children, selector, strictError); + }); +} + +export function convertLegacyApp( + rootElement: React.JSX.Element, +): (ExtensionOverrides | BackstagePlugin)[] { + const appRouterEls = selectChildren( + rootElement, + el => getComponentData(el, 'core.type') === 'AppRouter', + ); + if (appRouterEls.length !== 1) { + throw new Error( + "Failed to convert legacy app, AppRouter element could not been found. Make sure it's at the top level of the App element tree", + ); + } + + const rootEls = selectChildren( + appRouterEls[0].props.children, + el => + Boolean(el.props.children) && + selectChildren( + el.props.children, + innerEl => getComponentData(innerEl, 'core.type') === 'FlatRoutes', + ).length === 1, + ); + if (rootEls.length !== 1) { + throw new Error( + "Failed to convert legacy app, Root element containing FlatRoutes could not been found. Make sure it's within the AppRouter element of the App element tree", + ); + } + const [rootEl] = rootEls; + + const routesEls = selectChildren( + rootEls[0].props.children, + el => getComponentData(el, 'core.type') === 'FlatRoutes', + ); + if (routesEls.length !== 1) { + throw new Error( + 'Unexpectedly failed to find FlatRoutes in app element tree', + ); + } + const [routesEl] = routesEls; + + const CoreLayoutOverride = createExtension({ + id: 'core.layout', + attachTo: { id: 'core', input: 'root' }, + inputs: { + content: createExtensionInput( + { + element: coreExtensionData.reactElement, + }, + { singleton: true }, + ), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ bind, inputs }) { + // Clone the root element, this replaces the FlatRoutes declared in the app with out content input + bind({ + element: React.cloneElement(rootEl, undefined, inputs.content.element), + }); + }, + }); + const CoreNavOverride = createExtension({ + id: 'core.nav', + attachTo: { id: 'core.layout', input: 'nav' }, + output: {}, + factory() {}, + disabled: true, + }); + + const collectedRoutes = collectLegacyRoutes(routesEl); + + return [ + ...collectedRoutes, + createExtensionOverrides({ + extensions: [CoreLayoutOverride, CoreNavOverride], + }), + ]; +} diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index 2181b2fa9a..e5f61119a3 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { collectLegacyRoutes } from './collectLegacyRoutes'; +export { convertLegacyApp } from './convertLegacyApp'; From a4e72d309f11b2a0ac36f410c7dfc8ff167c75c2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 16:01:13 +0200 Subject: [PATCH 015/141] core-compat-api: add test for convertLegacyApp Signed-off-by: Patrik Oldsberg --- .../src/convertLegacyApp.test.tsx | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 packages/core-compat-api/src/convertLegacyApp.test.tsx diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx new file mode 100644 index 0000000000..0f1f6145c9 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx @@ -0,0 +1,129 @@ +/* + * Copyright 2023 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 { AppRouter, FlatRoutes } from '@backstage/core-app-api'; +import { PuppetDbPage } from '@backstage/plugin-puppetdb'; +import { StackstormPage } from '@backstage/plugin-stackstorm'; +import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; +import React, { ReactNode } from 'react'; +import { Route } from 'react-router-dom'; +import { convertLegacyApp } from './convertLegacyApp'; + +const Root = ({ children }: { children: ReactNode }) => <>{children}; + +describe('convertLegacyApp', () => { + it('should find and extract root and routes', () => { + const collected = convertLegacyApp( + <> +
+ + +
+ + + } /> + } /> + } /> + } /> + + + + , + ); + + expect( + collected.map((p: any /* TODO */) => ({ + id: p.id, + extensions: p.extensions.map((e: any) => ({ + id: e.id, + attachTo: e.attachTo, + disabled: e.disabled, + defaultConfig: e.configSchema?.parse({}), + })), + })), + ).toEqual([ + { + id: 'score-card', + extensions: [ + { + id: 'plugin.score-card.page', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'score-board' }, + }, + { + id: 'apis.plugin.scoringdata.service', + attachTo: { id: 'core', input: 'apis' }, + disabled: false, + }, + ], + }, + { + id: 'stackstorm', + extensions: [ + { + id: 'plugin.stackstorm.page', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'stackstorm' }, + }, + { + id: 'apis.plugin.stackstorm.service', + attachTo: { id: 'core', input: 'apis' }, + disabled: false, + }, + ], + }, + { + id: 'puppetDb', + extensions: [ + { + id: 'plugin.puppetDb.page', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'puppetdb' }, + }, + { + id: 'plugin.puppetDb.page2', + attachTo: { id: 'core.routes', input: 'routes' }, + disabled: false, + defaultConfig: { path: 'puppetdb' }, + }, + { + id: 'apis.plugin.puppetdb.service', + attachTo: { id: 'core', input: 'apis' }, + disabled: false, + }, + ], + }, + { + id: undefined, + extensions: [ + { + id: 'core.layout', + attachTo: { id: 'core', input: 'root' }, + disabled: false, + }, + { + id: 'core.nav', + attachTo: { id: 'core.layout', input: 'nav' }, + disabled: true, + }, + ], + }, + ]); + }); +}); From 43fcf6818c37bb3eb961c4a7e8f1d56c0a05d909 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 16:01:35 +0200 Subject: [PATCH 016/141] frontend-app-api: note in createExtensionInstance Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/wiring/createExtensionInstance.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts index 5a325dc0a0..767329da7f 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts @@ -63,6 +63,7 @@ function resolveInputs( const undeclaredAttachments = Array.from(attachments.entries()).filter( ([inputName]) => inputMap[inputName] === undefined, ); + // TODO: Make this a warning rather than an error if (undeclaredAttachments.length > 0) { throw new Error( `received undeclared input${ From 22b3b859a23e8447620bc8dc80a0b5c92a5d0a2c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 16 Oct 2023 08:49:18 +0200 Subject: [PATCH 017/141] feat(techdocs): export nav item and routes Signed-off-by: Camila Belo --- plugins/techdocs/src/alpha.tsx | 139 +++++++++++++++++---------------- 1 file changed, 73 insertions(+), 66 deletions(-) diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index f4d4da6e93..b7ccb0b12c 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -15,89 +15,33 @@ */ import React from 'react'; +import LibraryBooks from '@material-ui/icons/LibraryBooks'; import { createPlugin, createSchemaFromZod, createApiExtension, createPageExtension, + createNavItemExtension, } from '@backstage/frontend-plugin-api'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; import { configApiRef, createApiFactory, - createRouteRef, discoveryApiRef, fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; import { techdocsApiRef, techdocsStorageApiRef, } from '@backstage/plugin-techdocs-react'; import { TechDocsClient, TechDocsStorageClient } from './client'; -import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; - -const rootRouteRef = createRouteRef({ - id: 'plugin.techdocs.indexPage', -}); - -const rootDocsRouteRef = createRouteRef({ - id: 'plugin.techdocs.readerPage', - params: ['namespace', 'kind', 'name'], -}); - -/** @alpha */ -export const TechDocsSearchResultListItemExtension = - createSearchResultListItemExtension({ - id: 'techdocs', - configSchema: createSchemaFromZod(z => - z.object({ - // TODO: Define how the icon can be configurable - title: z.string().optional(), - lineClamp: z.number().default(5), - asLink: z.boolean().default(true), - asListItem: z.boolean().default(true), - noTrack: z.boolean().default(false), - }), - ), - predicate: result => result.type === 'techdocs', - component: async ({ config }) => { - const { TechDocsSearchResultListItem } = await import( - './search/components/TechDocsSearchResultListItem' - ); - return props => ; - }, - }); - -/** - * Responsible for rendering the provided router element - * - * @alpha - */ -const TechDocsIndexPage = createPageExtension({ - id: 'plugin.techdocs.indexPage', - defaultPath: '/docs', - routeRef: convertLegacyRouteRef(rootRouteRef), - loader: () => - import('./home/components/TechDocsIndexPage').then(m => ( - - )), -}); - -/** - * Component responsible for composing a TechDocs reader page experience - * - * @alpha - */ -const TechDocsReaderPage = createPageExtension({ - id: 'plugin.techdocs.readerPage', - loader: () => - import('./reader/components/TechDocsReaderPage').then(m => ( - - )), - routeRef: convertLegacyRouteRef(rootDocsRouteRef), - defaultPath: '/docs/:namespace/:kind/:name/*', -}); +import { + rootCatalogDocsRouteRef, + rootDocsRouteRef, + rootRouteRef, +} from './routes'; /** @alpha */ const techDocsStorage = createApiExtension({ @@ -144,18 +88,81 @@ const techDocsClient = createApiExtension({ }, }); +/** @alpha */ +export const TechDocsSearchResultListItemExtension = + createSearchResultListItemExtension({ + id: 'techdocs', + configSchema: createSchemaFromZod(z => + z.object({ + // TODO: Define how the icon can be configurable + title: z.string().optional(), + lineClamp: z.number().default(5), + asLink: z.boolean().default(true), + asListItem: z.boolean().default(true), + noTrack: z.boolean().default(false), + }), + ), + predicate: result => result.type === 'techdocs', + component: async ({ config }) => { + const { TechDocsSearchResultListItem } = await import( + './search/components/TechDocsSearchResultListItem' + ); + return props => ; + }, + }); + +/** + * Responsible for rendering the provided router element + * + * @alpha + */ +const TechDocsIndexPage = createPageExtension({ + id: 'plugin.techdocs.indexPage', + defaultPath: '/docs', + routeRef: convertLegacyRouteRef(rootRouteRef), + loader: () => + import('./home/components/TechDocsIndexPage').then(m => ( + + )), +}); + +/** + * Component responsible for composing a TechDocs reader page experience + * + * @alpha + */ +const TechDocsReaderPage = createPageExtension({ + id: 'plugin.techdocs.readerPage', + defaultPath: '/docs/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(rootDocsRouteRef), + loader: () => + import('./reader/components/TechDocsReaderPage').then(m => ( + + )), +}); + +/** @alpha */ +const TechDocsNavItem = createNavItemExtension({ + id: 'plugin.techdocs.nav.index', + icon: LibraryBooks, + title: 'Docs', + routeRef: convertLegacyRouteRef(rootRouteRef), +}); + /** @alpha */ export default createPlugin({ id: 'techdocs', extensions: [ - TechDocsIndexPage, - TechDocsReaderPage, techDocsClient, techDocsStorage, + TechDocsNavItem, + TechDocsIndexPage, + TechDocsReaderPage, TechDocsSearchResultListItemExtension, ], routes: { root: convertLegacyRouteRef(rootRouteRef), docRoot: convertLegacyRouteRef(rootDocsRouteRef), + entityContent: convertLegacyRouteRef(rootCatalogDocsRouteRef), }, }); From 8b6a98921d4274e3da6d84099d7570604cf171c6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 16 Oct 2023 08:49:53 +0200 Subject: [PATCH 018/141] docs(techdocs): update api reports Signed-off-by: Camila Belo --- plugins/techdocs/alpha-api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/techdocs/alpha-api-report.md b/plugins/techdocs/alpha-api-report.md index 2b3a934e34..2b63d6e691 100644 --- a/plugins/techdocs/alpha-api-report.md +++ b/plugins/techdocs/alpha-api-report.md @@ -16,6 +16,7 @@ const _default: BackstagePlugin< kind: string; namespace: string; }>; + entityContent: RouteRef; }, {} >; From a3add7a6829110af554635fd93cee3e93ec6e9e6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 18 Oct 2023 18:37:28 +0200 Subject: [PATCH 019/141] docs: add changeset file Signed-off-by: Camila Belo --- .changeset/giant-cars-walk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/giant-cars-walk.md diff --git a/.changeset/giant-cars-walk.md b/.changeset/giant-cars-walk.md new file mode 100644 index 0000000000..5e313faa3b --- /dev/null +++ b/.changeset/giant-cars-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Export alpha routes and nav item extension, only available for applications that uses the new Frontend system. From ad2de37115f3202981988efe9156e598059a13cb Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Wed, 18 Oct 2023 13:59:12 -0400 Subject: [PATCH 020/141] refactor Signed-off-by: Sydney Achinger --- .../TechDocsReaderPageContent/TechDocsReaderPageContent.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx index 32fe788716..8508c59142 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx @@ -86,8 +86,10 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider( const [hashElement] = useShadowRootElements([`[id="${hash.slice(1)}"]`]); useEffect(() => { - if (hashElement && !isStyleLoading) { - hashElement.scrollIntoView(); + if (hash) { + if (hashElement && !isStyleLoading) { + hashElement.scrollIntoView(); + } } else { document?.querySelector('header')?.scrollIntoView(); } From b1ff135ba68150dec215e51c909d3f7f883e2b80 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 18 Oct 2023 15:48:58 -0500 Subject: [PATCH 021/141] chore: updated Keyloop adopter contact Signed-off-by: Andre Wanlin --- ADOPTERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index b5e19106b7..f0d6e4ed0a 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -78,7 +78,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | | [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | | [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | -| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | +| [Keyloop](https://www.keyloop.com/) | [Shawn Bruce](https://github.com/sbruce-keyloop) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | | [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. | | [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding | | [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. | From 4b5618330acbc7eed8c39f7b037ee1067d46d851 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 18 Oct 2023 15:54:12 -0500 Subject: [PATCH 022/141] chore: updated Owner Org for awanlin Signed-off-by: Andre Wanlin --- OWNERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OWNERS.md b/OWNERS.md index caf5bc4d14..2ea263f3b4 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -145,7 +145,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | ------------------------------ | ------------------------- | ----------------------------------------------------- | ------------------------------ | | Adam Harvey | Cisco | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey_` | | Alex Crome | | [afscrome](https://github.com/afscrome) | `afscrome` | -| Andre Wanlin | Keyloop | [awanlin](https://github.com/awanlin) | `ahhhndre` | +| Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | | Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` | | Aramis Sennyey | Spotify | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | | Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` | From dd4f4f06f5bbcc1a755d576a3f9a2577f19ff108 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 01:02:27 +0200 Subject: [PATCH 023/141] core-compat-api: update api report + fix Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/api-report.md | 7 +++++++ packages/core-compat-api/src/convertLegacyApp.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index 5175c76ac8..3dd2eb6b22 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -6,11 +6,18 @@ /// import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { ExtensionOverrides } from '@backstage/frontend-plugin-api'; +import { default as React_2 } from 'react'; // @public (undocumented) export function collectLegacyRoutes( flatRoutesElement: JSX.Element, ): BackstagePlugin[]; +// @public (undocumented) +export function convertLegacyApp( + rootElement: React_2.JSX.Element, +): (ExtensionOverrides | BackstagePlugin)[]; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index 9343744075..59cb136c65 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -58,6 +58,7 @@ function selectChildren( }); } +/** @public */ export function convertLegacyApp( rootElement: React.JSX.Element, ): (ExtensionOverrides | BackstagePlugin)[] { From 47dc8b84b3bb70fbb94a2b748a35e5da2054171f Mon Sep 17 00:00:00 2001 From: Ankit Anand Date: Thu, 19 Oct 2023 04:43:24 +0530 Subject: [PATCH 024/141] removed mock-fs in scaffolder rails Signed-off-by: Ankit Anand --- .../package.json | 5 ++--- .../src/actions/fetch/rails/index.test.ts | 17 +++++++++-------- yarn.lock | 3 +-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 2bc91478e4..0763690d7c 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -39,13 +39,12 @@ "fs-extra": "^10.0.1" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", - "@types/mock-fs": "^4.13.0", "@types/node": "^18.17.8", - "jest-when": "^3.1.0", - "mock-fs": "^5.2.0" + "jest-when": "^3.1.0" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 2a93644f57..10b8466dc2 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -34,14 +34,14 @@ import { } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import mockFs from 'mock-fs'; -import os from 'os'; import { resolve as resolvePath } from 'path'; import { PassThrough } from 'stream'; import { createFetchRailsAction } from './index'; import { fetchContents } from '@backstage/plugin-scaffolder-node'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fetch:rails', () => { + const mockDir = createMockDirectory(); const integrations = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -53,7 +53,7 @@ describe('fetch:rails', () => { }), ); - const mockTmpDir = os.tmpdir(); + const mockTmpDir = mockDir.path; const mockContext = { input: { url: 'https://rubyonrails.org/generator', @@ -73,6 +73,9 @@ describe('fetch:rails', () => { createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; + mockDir.clear(); + mockDir.addContent({ template: {} }); + const mockReader: UrlReader = { readUrl: jest.fn(), readTree: jest.fn(), @@ -90,14 +93,12 @@ describe('fetch:rails', () => { }); beforeEach(() => { - mockFs({ [`${mockContext.workspacePath}/result`]: {} }); + mockDir.addContent({ + result: '{}', + }); jest.clearAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should call fetchContents with the correct values', async () => { await action.handler(mockContext); diff --git a/yarn.lock b/yarn.lock index 636d159dd3..5c88d57903 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8494,6 +8494,7 @@ __metadata: resolution: "@backstage/plugin-scaffolder-backend-module-rails@workspace:plugins/scaffolder-backend-module-rails" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -8502,12 +8503,10 @@ __metadata: "@backstage/types": "workspace:^" "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 - "@types/mock-fs": ^4.13.0 "@types/node": ^18.17.8 command-exists: ^1.2.9 fs-extra: ^10.0.1 jest-when: ^3.1.0 - mock-fs: ^5.2.0 languageName: unknown linkType: soft From 43a2ff70cbe3b59f62cc4356fc03056bad82d4e8 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Wed, 18 Oct 2023 18:43:08 -0500 Subject: [PATCH 025/141] docs: new backend system updates Signed-off-by: Andre Wanlin --- .../architecture/02-backends.md | 2 +- .../building-backends/01-index.md | 12 ++++++------ .../building-backends/08-migrating.md | 19 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/backend-system/architecture/02-backends.md b/docs/backend-system/architecture/02-backends.md index 4ba76a5af3..c64024d653 100644 --- a/docs/backend-system/architecture/02-backends.md +++ b/docs/backend-system/architecture/02-backends.md @@ -23,7 +23,7 @@ import scaffolderPlugin from '@backstage/plugin-scaffolder-backend'; const backend = createBackend(); // Install desired features -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); // Features can also be installed using an explicit reference backend.add(scaffolderPlugin()); diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index 7d331b4ec3..c381999429 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -24,9 +24,9 @@ import { createBackend } from '@backstage/backend-defaults'; // Omitted in the e const backend = createBackend(); -backend.add(import('@backstage/plugin-app-backend')); -backend.add(import('@backstage/plugin-catalog-backend')); -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-app-backend/alpha')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -126,8 +126,8 @@ You can now trim down the `src/index.ts` files to only include the plugins and m ```ts const backend = createBackend(); -backend.add(import('@backstage/plugin-app-backend')); -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-app-backend/alpha')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -139,7 +139,7 @@ And `backend-b`, don't forget to clean up dependencies in `package.json` as well ```ts const backend = createBackend(); -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.start(); ``` diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 1a380421c1..1e30d0aa9e 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -177,11 +177,10 @@ custom API, so we use a helper function to transform that particular one. To make additions as mentioned above to the environment, you will start to get into the weeds of how the backend system wiring works. You'll need to have a service reference and a service factory that performs the actual creation of -your service. Please see [the services -article](../architecture/03-services.md#defining-a-service) to learn how to -create a service ref and its default factory. You can place that code directly -in the index file for now if you want, or near the actual implementation class -in question. +your service. Please see [the services article](../architecture/03-services.md) +to learn how to create a service ref and its default factory. You can place that +code directly in the index file for now if you want, or near the actual implementation +class in question. In this example, we'll assume that your added environment field is named `example`, and the created ref is named `exampleServiceRef`. @@ -233,7 +232,7 @@ be used in its new form. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-next-line */ -backend.add(import('@backstage/plugin-app-backend')); +backend.add(import('@backstage/plugin-app-backend/alpha')); ``` If you need to override the app package name, which otherwise defaults to `"app"`, @@ -248,7 +247,7 @@ A basic installation of the catalog plugin looks as follows. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-start */ -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -296,7 +295,7 @@ const catalogModuleCustomExtensions = createBackendModule({ /* highlight-add-end */ const backend = createBackend(); -backend.add(import('@backstage/plugin-catalog-backend')); +backend.add(import('@backstage/plugin-catalog-backend/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); @@ -390,7 +389,7 @@ A basic installation of the scaffolder plugin looks as follows. ```ts title="packages/backend/src/index.ts" const backend = createBackend(); /* highlight-add-next-line */ -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); ``` If you have other customizations made to `plugins/scaffolder.ts`, such as adding @@ -429,7 +428,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({ /* highlight-add-end */ const backend = createBackend(); -backend.add(import('@backstage/plugin-scaffolder-backend')); +backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); /* highlight-add-next-line */ backend.add(scaffolderModuleCustomExtensions()); ``` From ad67a33d099cc4689778f37778bf2b169a525f26 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:22:23 +0200 Subject: [PATCH 026/141] fix(search-backend-node): non-string field hightlight Signed-off-by: Camila Belo --- .../search-backend-node/src/engines/LunrSearchEngine.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index ae5a541e81..02bd6f7fd7 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -329,11 +329,11 @@ export function parseHighlightFields({ const highlightedField = positions.reduce((content, pos) => { return ( - `${content.substring(0, pos[0])}${preTag}` + - `${content.substring(pos[0], pos[0] + pos[1])}` + - `${postTag}${content.substring(pos[0] + pos[1])}` + `${content.toString().substring(0, pos[0])}${preTag}` + + `${content.toString().substring(pos[0], pos[0] + pos[1])}` + + `${postTag}${content.toString().substring(pos[0] + pos[1])}` ); - }, doc[field]); + }, doc[field] ?? ''); return [field, highlightedField]; }), From e8839fe017b46cf49ff2eede1776e819df205e8c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:26:12 +0200 Subject: [PATCH 027/141] feat(stackoverflow-backend): get request params from config Signed-off-by: Camila Belo --- plugins/stack-overflow-backend/config.d.ts | 7 +++++++ .../search/StackOverflowQuestionsCollatorFactory.ts | 13 +++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/plugins/stack-overflow-backend/config.d.ts b/plugins/stack-overflow-backend/config.d.ts index b97f237c19..613a7aa887 100644 --- a/plugins/stack-overflow-backend/config.d.ts +++ b/plugins/stack-overflow-backend/config.d.ts @@ -40,5 +40,12 @@ export interface Config { * @visibility secret */ apiAccessToken?: string; + + /** + * Type representing the request parameters. + */ + requestParams?: { + [key: string]: string | string[] | number; + }; }; } diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts index c5f8a8c16b..cf1e56cb28 100644 --- a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts +++ b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts @@ -54,7 +54,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = { apiKey?: string; apiAccessToken?: string; teamName?: string; - requestParams: StackOverflowQuestionsRequestParams; + requestParams?: StackOverflowQuestionsRequestParams; logger: Logger; }; @@ -81,7 +81,11 @@ export class StackOverflowQuestionsCollatorFactory this.apiAccessToken = options.apiAccessToken; this.teamName = options.teamName; this.maxPage = options.maxPage; - this.requestParams = options.requestParams; + this.requestParams = options.requestParams ?? { + tagged: ['backstage'], + site: 'stackoverflow', + pagesize: 100, + }; this.logger = options.logger.child({ documentType: this.type }); } @@ -98,12 +102,17 @@ export class StackOverflowQuestionsCollatorFactory config.getOptionalString('stackoverflow.baseUrl') || 'https://api.stackexchange.com/2.3'; const maxPage = options.maxPage || 100; + const requestParams = + config.getOptional( + 'stackoverflow.requestParams', + ); return new StackOverflowQuestionsCollatorFactory({ baseUrl, maxPage, apiKey, apiAccessToken, teamName, + requestParams, ...options, }); } From 43bcd9fcead0ae6cc9ac4a3e2b5e7f2441fa2ba5 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:27:50 +0200 Subject: [PATCH 028/141] feat(stackoverflow-backend): migrate collator to backend system Signed-off-by: Camila Belo --- .../alpha-api-report.md | 11 +++ plugins/stack-overflow-backend/api-report.md | 2 +- plugins/stack-overflow-backend/package.json | 23 ++++-- plugins/stack-overflow-backend/src/alpha.ts | 70 +++++++++++++++++++ 4 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 plugins/stack-overflow-backend/alpha-api-report.md create mode 100644 plugins/stack-overflow-backend/src/alpha.ts diff --git a/plugins/stack-overflow-backend/alpha-api-report.md b/plugins/stack-overflow-backend/alpha-api-report.md new file mode 100644 index 0000000000..93d4cfe5e5 --- /dev/null +++ b/plugins/stack-overflow-backend/alpha-api-report.md @@ -0,0 +1,11 @@ +## API Report File for "@backstage/plugin-stack-overflow-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; + +// @alpha +const _default: () => BackendFeature; +export default _default; +``` diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md index 8c601399c1..e8fd710cf6 100644 --- a/plugins/stack-overflow-backend/api-report.md +++ b/plugins/stack-overflow-backend/api-report.md @@ -45,7 +45,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = { apiKey?: string; apiAccessToken?: string; teamName?: string; - requestParams: StackOverflowQuestionsRequestParams; + requestParams?: StackOverflowQuestionsRequestParams; logger: Logger; }; diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 320d35d3b6..53857082d2 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -5,9 +5,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "backend-plugin" @@ -33,7 +46,10 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "node-fetch": "^2.6.7", "qs": "^6.9.4", @@ -42,7 +58,6 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@backstage/plugin-search-backend-node": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow-backend/src/alpha.ts b/plugins/stack-overflow-backend/src/alpha.ts new file mode 100644 index 0000000000..897cbd1ea2 --- /dev/null +++ b/plugins/stack-overflow-backend/src/alpha.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2023 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 { loggerToWinstonLogger } from '@backstage/backend-common'; +import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks'; +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import { StackOverflowQuestionsCollatorFactory } from './search'; + +/** + * @packageDocumentation + * A module for the search backend that exports Stack Overflow modules. + */ + +/** + * Search backend module for the Stack Overflow index. + * + * @alpha + */ +export default createBackendModule({ + moduleId: 'stackoverflowCollator', + pluginId: 'search', + register(env) { + env.registerInit({ + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + discovery: coreServices.discovery, + scheduler: coreServices.scheduler, + indexRegistry: searchIndexRegistryExtensionPoint, + }, + async init({ config, logger, scheduler, indexRegistry }) { + const defaultSchedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; + + const schedule = config.has('search.collators.stackoverflow.schedule') + ? readTaskScheduleDefinitionFromConfig( + config.getConfig('search.collators.stackoverflow.schedule'), + ) + : defaultSchedule; + + indexRegistry.addCollator({ + schedule: scheduler.createScheduledTaskRunner(schedule), + factory: StackOverflowQuestionsCollatorFactory.fromConfig(config, { + logger: loggerToWinstonLogger(logger), + }), + }); + }, + }); + }, +}); From 41c2cdf23358e13b29303e92df0da3c4674fd9cb Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:32:36 +0200 Subject: [PATCH 029/141] feat(backend-next): install stack overflow search module Signed-off-by: Camila Belo --- packages/backend-next/package.json | 1 + packages/backend-next/src/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index 69aa81a9cb..41864907e5 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -56,6 +56,7 @@ "@backstage/plugin-search-backend-module-techdocs": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-sonarqube-backend": "workspace:^", + "@backstage/plugin-stack-overflow-backend": "workspace:^", "@backstage/plugin-techdocs-backend": "workspace:^", "@backstage/plugin-todo-backend": "workspace:^" }, diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index bdfbc2c26b..15f5ca9c38 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -46,6 +46,7 @@ backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); +backend.add(import('@backstage/plugin-stack-overflow-backend/alpha')); backend.add(import('@backstage/plugin-todo-backend')); backend.add(import('@backstage/plugin-sonarqube-backend')); From b148cdc8dbfc8889371ee221a2a1ba6075a16cf2 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:38:24 +0200 Subject: [PATCH 030/141] feat(stackoverflow): migrate to new frontend system Signed-off-by: Camila Belo --- plugins/stack-overflow/alpha-api-report.md | 13 ++++++ plugins/stack-overflow/package.json | 20 +++++++-- plugins/stack-overflow/src/alpha.tsx | 48 ++++++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 plugins/stack-overflow/alpha-api-report.md create mode 100644 plugins/stack-overflow/src/alpha.tsx diff --git a/plugins/stack-overflow/alpha-api-report.md b/plugins/stack-overflow/alpha-api-report.md new file mode 100644 index 0000000000..84d7616759 --- /dev/null +++ b/plugins/stack-overflow/alpha-api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-stack-overflow" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstagePlugin } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +const _default: BackstagePlugin; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index f60800d1f9..431f8a5b80 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -5,9 +5,22 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.tsx" + ], + "package.json": [ + "package.json" + ] + } }, "backstage": { "role": "frontend-plugin" @@ -32,6 +45,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-home-react": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "@backstage/plugin-search-react": "workspace:^", diff --git a/plugins/stack-overflow/src/alpha.tsx b/plugins/stack-overflow/src/alpha.tsx new file mode 100644 index 0000000000..18f0a0df1c --- /dev/null +++ b/plugins/stack-overflow/src/alpha.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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 { configApiRef, createApiFactory } from '@backstage/core-plugin-api'; +import { + createApiExtension, + createPlugin, +} from '@backstage/frontend-plugin-api'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { StackOverflowClient, stackOverflowApiRef } from './api'; + +/** @alpha */ +const StackOverflowApi = createApiExtension({ + factory: createApiFactory({ + api: stackOverflowApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => StackOverflowClient.fromConfig(configApi), + }), +}); + +/** @alpha */ +const StackOverflowSearchResultListItem = createSearchResultListItemExtension({ + id: 'stack-overflow', + predicate: result => result.type === 'stack-overflow', + component: () => + import('./search/StackOverflowSearchResultListItem').then( + m => m.StackOverflowSearchResultListItem, + ), +}); + +/** @alpha */ +export default createPlugin({ + id: 'stack-overflow', + extensions: [StackOverflowApi, StackOverflowSearchResultListItem], +}); From 8b44bf58f2380d4d0c3806227325397aa5a04259 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 11:41:12 +0200 Subject: [PATCH 031/141] feat(search): add stackoverflow to types accordion Signed-off-by: Camila Belo --- plugins/search/src/alpha.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 14e7067dfa..913a96d094 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -69,6 +69,25 @@ import { SearchType } from './components/SearchType'; import { UrlUpdater } from './components/SearchPage/SearchPage'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +/** @internal */ +const StackOverflowIcon = () => { + return ( + + + + + ); +}; + /** @alpha */ export const SearchApi = createApiExtension({ factory: { @@ -153,6 +172,11 @@ export const SearchPage = createPageExtension({ name: 'Architecture Decision Records', icon: , }, + { + value: 'stack-overflow', + name: 'Stack Overflow', + icon: , + }, ]} /> From b168d7e7ea16c440d26d68fc0663c5ce6ece71a3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sun, 8 Oct 2023 12:55:20 +0200 Subject: [PATCH 032/141] chore: add changeset files Signed-off-by: Camila Belo --- .changeset/flat-ducks-buy.md | 5 +++++ .changeset/giant-cycles-end.md | 5 +++++ .changeset/mean-fans-cough.md | 5 +++++ .changeset/nice-pillows-poke.md | 5 +++++ 4 files changed, 20 insertions(+) create mode 100644 .changeset/flat-ducks-buy.md create mode 100644 .changeset/giant-cycles-end.md create mode 100644 .changeset/mean-fans-cough.md create mode 100644 .changeset/nice-pillows-poke.md diff --git a/.changeset/flat-ducks-buy.md b/.changeset/flat-ducks-buy.md new file mode 100644 index 0000000000..5c952faaf1 --- /dev/null +++ b/.changeset/flat-ducks-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Fix highlighting for non-string fields on the `Lunr` search engine implementation. diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md new file mode 100644 index 0000000000..6cd21fe54e --- /dev/null +++ b/.changeset/giant-cycles-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow-backend': patch +--- + +Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. diff --git a/.changeset/mean-fans-cough.md b/.changeset/mean-fans-cough.md new file mode 100644 index 0000000000..8f93cd361c --- /dev/null +++ b/.changeset/mean-fans-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Add Stack Overflow type to the default alpha search page extension types filter. diff --git a/.changeset/nice-pillows-poke.md b/.changeset/nice-pillows-poke.md new file mode 100644 index 0000000000..1a3926f371 --- /dev/null +++ b/.changeset/nice-pillows-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow': patch +--- + +Migrate package to the new Backend system, the new module is distributed with a `/alpha` subpath. From dad7d4de5b0b0c1a6307b57cfbc75585a4666fe1 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 08:17:02 +0200 Subject: [PATCH 033/141] chore: update yarn lock Signed-off-by: Camila Belo --- yarn.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 1e2f6fdb5d..bc7aa10d86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9182,11 +9182,13 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": +"@backstage/plugin-stack-overflow-backend@workspace:^, @backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -9209,6 +9211,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-home-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" @@ -25502,6 +25505,7 @@ __metadata: "@backstage/plugin-search-backend-module-techdocs": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-sonarqube-backend": "workspace:^" + "@backstage/plugin-stack-overflow-backend": "workspace:^" "@backstage/plugin-techdocs-backend": "workspace:^" "@backstage/plugin-todo-backend": "workspace:^" languageName: unknown From 847841a89d766ce460039b7202e5c7ef23286e02 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 14:57:42 +0200 Subject: [PATCH 034/141] refactor(stack-overflow-backend): use official default request parameters Signed-off-by: Camila Belo --- .changeset/giant-cycles-end.md | 2 ++ plugins/stack-overflow-backend/README.md | 2 +- .../StackOverflowQuestionsCollatorFactory.ts | 16 +++++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md index 6cd21fe54e..5d9f8149a8 100644 --- a/.changeset/giant-cycles-end.md +++ b/.changeset/giant-cycles-end.md @@ -3,3 +3,5 @@ --- Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. + +The search collator `requestParams` option is optional now, so its defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md index 4575ab58c0..5de997faa6 100644 --- a/plugins/stack-overflow-backend/README.md +++ b/plugins/stack-overflow-backend/README.md @@ -45,7 +45,7 @@ This stack overflow backend plugin is primarily responsible for the following: Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). -When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can modify the `requestParams`. +When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). > Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions) diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts index cf1e56cb28..3c97937365 100644 --- a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts +++ b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts @@ -81,10 +81,13 @@ export class StackOverflowQuestionsCollatorFactory this.apiAccessToken = options.apiAccessToken; this.teamName = options.teamName; this.maxPage = options.maxPage; + // Sets the same default request parameters as the official API documentation + // See https://api.stackexchange.com/docs/questions this.requestParams = options.requestParams ?? { - tagged: ['backstage'], + order: 'desc', + sort: 'activity', site: 'stackoverflow', - pagesize: 100, + ...(options.requestParams ?? {}), }; this.logger = options.logger.child({ documentType: this.type }); } @@ -102,10 +105,9 @@ export class StackOverflowQuestionsCollatorFactory config.getOptionalString('stackoverflow.baseUrl') || 'https://api.stackexchange.com/2.3'; const maxPage = options.maxPage || 100; - const requestParams = - config.getOptional( - 'stackoverflow.requestParams', - ); + const requestParams = config + .getOptionalConfig('stackoverflow.requestParams') + ?.get(); return new StackOverflowQuestionsCollatorFactory({ baseUrl, maxPage, @@ -184,7 +186,7 @@ export class StackOverflowQuestionsCollatorFactory ); const data = await res.json(); - for (const question of data.items) { + for (const question of data.items ?? []) { yield { title: question.title, location: question.link, From 7b1296c57a4c5659c9c010b17ca3591869985b2b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 15:06:01 +0200 Subject: [PATCH 035/141] refactor(search): use string construction for non-text values Signed-off-by: Camila Belo --- plugins/search-backend-node/src/engines/LunrSearchEngine.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index 02bd6f7fd7..27cfae3fdf 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -329,9 +329,9 @@ export function parseHighlightFields({ const highlightedField = positions.reduce((content, pos) => { return ( - `${content.toString().substring(0, pos[0])}${preTag}` + - `${content.toString().substring(pos[0], pos[0] + pos[1])}` + - `${postTag}${content.toString().substring(pos[0] + pos[1])}` + `${String(content).substring(0, pos[0])}${preTag}` + + `${String(content).substring(pos[0], pos[0] + pos[1])}` + + `${postTag}${String(content).substring(pos[0] + pos[1])}` ); }, doc[field] ?? ''); From a8a4911713c4b2f4ac4da0031caef7b1def9594f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 15:49:21 +0200 Subject: [PATCH 036/141] refactor: do not install stack overflow plugin on example apps Signed-off-by: Camila Belo --- packages/app-next/package.json | 1 - packages/backend-next/package.json | 1 - packages/backend-next/src/index.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 71d888ee50..5437c66272 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -68,7 +68,6 @@ "@backstage/plugin-search-react": "workspace:^", "@backstage/plugin-sentry": "workspace:^", "@backstage/plugin-shortcuts": "workspace:^", - "@backstage/plugin-stack-overflow": "workspace:^", "@backstage/plugin-stackstorm": "workspace:^", "@backstage/plugin-tech-insights": "workspace:^", "@backstage/plugin-tech-radar": "workspace:^", diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index 41864907e5..69aa81a9cb 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -56,7 +56,6 @@ "@backstage/plugin-search-backend-module-techdocs": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@backstage/plugin-sonarqube-backend": "workspace:^", - "@backstage/plugin-stack-overflow-backend": "workspace:^", "@backstage/plugin-techdocs-backend": "workspace:^", "@backstage/plugin-todo-backend": "workspace:^" }, diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index 15f5ca9c38..bdfbc2c26b 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -46,7 +46,6 @@ backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); -backend.add(import('@backstage/plugin-stack-overflow-backend/alpha')); backend.add(import('@backstage/plugin-todo-backend')); backend.add(import('@backstage/plugin-sonarqube-backend')); From 27e22c6a21b33b6cc0162995053c5730fcad8a91 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 16:19:16 +0200 Subject: [PATCH 037/141] feat(stackoverflow-backend): extract search stack overflow module Signed-off-by: Camila Belo --- .../.eslintrc.js | 1 + .../README.md | 30 +++++++++ .../alpha-api-report.md | 2 +- .../api-report.md | 56 ++++++++++++++++ .../catalog-info.yaml | 10 +++ .../config.d.ts | 51 +++++++++++++++ .../package.json | 65 +++++++++++++++++++ .../src/alpha.test.ts | 55 ++++++++++++++++ .../src/alpha.ts | 6 +- ...ckOverflowQuestionsCollatorFactory.test.ts | 0 .../StackOverflowQuestionsCollatorFactory.ts | 0 .../src/collators/index.ts | 22 +++++++ .../src}/index.ts | 9 ++- plugins/stack-overflow-backend/api-report.md | 59 ++++------------- plugins/stack-overflow-backend/package.json | 24 ++----- plugins/stack-overflow-backend/src/index.ts | 38 ++++++++++- yarn.lock | 26 ++++++-- 17 files changed, 377 insertions(+), 77 deletions(-) create mode 100644 plugins/search-backend-module-stack-overflow/.eslintrc.js create mode 100644 plugins/search-backend-module-stack-overflow/README.md rename plugins/{stack-overflow-backend => search-backend-module-stack-overflow}/alpha-api-report.md (75%) create mode 100644 plugins/search-backend-module-stack-overflow/api-report.md create mode 100644 plugins/search-backend-module-stack-overflow/catalog-info.yaml create mode 100644 plugins/search-backend-module-stack-overflow/config.d.ts create mode 100644 plugins/search-backend-module-stack-overflow/package.json create mode 100644 plugins/search-backend-module-stack-overflow/src/alpha.test.ts rename plugins/{stack-overflow-backend => search-backend-module-stack-overflow}/src/alpha.ts (90%) rename plugins/{stack-overflow-backend/src/search => search-backend-module-stack-overflow/src/collators}/StackOverflowQuestionsCollatorFactory.test.ts (100%) rename plugins/{stack-overflow-backend/src/search => search-backend-module-stack-overflow/src/collators}/StackOverflowQuestionsCollatorFactory.ts (100%) create mode 100644 plugins/search-backend-module-stack-overflow/src/collators/index.ts rename plugins/{stack-overflow-backend/src/search => search-backend-module-stack-overflow/src}/index.ts (76%) diff --git a/plugins/search-backend-module-stack-overflow/.eslintrc.js b/plugins/search-backend-module-stack-overflow/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-module-stack-overflow/README.md b/plugins/search-backend-module-stack-overflow/README.md new file mode 100644 index 0000000000..cba9617ea8 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/README.md @@ -0,0 +1,30 @@ +# search-backend-module-stack-overflow + +> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. + +This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions). + +## Installation + +Add the module package as a dependency: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow +``` + +Add the collator to your backend instance, along with the search plugin itself: + +```tsx +// packages/backend/src/index.ts +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.add(import('@backstage/plugin-search-backend/alpha')); +backend.add( + import('@backstage/plugin-search-backend-module-stack-overflow/alpha'), +); +backend.start(); +``` + +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. diff --git a/plugins/stack-overflow-backend/alpha-api-report.md b/plugins/search-backend-module-stack-overflow/alpha-api-report.md similarity index 75% rename from plugins/stack-overflow-backend/alpha-api-report.md rename to plugins/search-backend-module-stack-overflow/alpha-api-report.md index 93d4cfe5e5..9d25cb0a7d 100644 --- a/plugins/stack-overflow-backend/alpha-api-report.md +++ b/plugins/search-backend-module-stack-overflow/alpha-api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-stack-overflow-backend" +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/search-backend-module-stack-overflow/api-report.md b/plugins/search-backend-module-stack-overflow/api-report.md new file mode 100644 index 0000000000..795b30cac2 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/api-report.md @@ -0,0 +1,56 @@ +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { Config } from '@backstage/config'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; +import { Readable } from 'stream'; + +// @public +export interface StackOverflowDocument extends IndexableDocument { + // (undocumented) + answers: number; + // (undocumented) + tags: string[]; +} + +// @public +export class StackOverflowQuestionsCollatorFactory + implements DocumentCollatorFactory +{ + // (undocumented) + execute(): AsyncGenerator; + // (undocumented) + static fromConfig( + config: Config, + options: StackOverflowQuestionsCollatorFactoryOptions, + ): StackOverflowQuestionsCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + protected requestParams: StackOverflowQuestionsRequestParams; + // (undocumented) + readonly type: string; +} + +// @public +export type StackOverflowQuestionsCollatorFactoryOptions = { + baseUrl?: string; + maxPage?: number; + apiKey?: string; + apiAccessToken?: string; + teamName?: string; + requestParams?: StackOverflowQuestionsRequestParams; + logger: Logger; +}; + +// @public +export type StackOverflowQuestionsRequestParams = { + [key: string]: string | string[] | number; +}; +``` diff --git a/plugins/search-backend-module-stack-overflow/catalog-info.yaml b/plugins/search-backend-module-stack-overflow/catalog-info.yaml new file mode 100644 index 0000000000..9075d1d591 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-search-backend-module-stack-overflow + title: '@backstage/plugin-search-backend-module-stack-overflow' + description: A module for the search backend that exports stack overflow modules +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: discoverability-maintainers diff --git a/plugins/search-backend-module-stack-overflow/config.d.ts b/plugins/search-backend-module-stack-overflow/config.d.ts new file mode 100644 index 0000000000..d615179ed0 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/config.d.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2023 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 interface Config { + /** + * Configuration options for the stack overflow plugin + */ + stackoverflow?: { + /** + * The base url of the Stack Overflow API used for the plugin + */ + baseUrl?: string; + + /** + * The API key to authenticate to Stack Overflow API + * @visibility secret + */ + apiKey?: string; + + /** + * The name of the team for a Stack Overflow for Teams account + */ + teamName?: string; + + /** + * The API Access Token to authenticate to Stack Overflow API + * @visibility secret + */ + apiAccessToken?: string; + + /** + * Type representing the request parameters. + */ + requestParams?: { + [key: string]: string | string[] | number; + }; + }; +} diff --git a/plugins/search-backend-module-stack-overflow/package.json b/plugins/search-backend-module-stack-overflow/package.json new file mode 100644 index 0000000000..64ff4d32d3 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/package.json @@ -0,0 +1,65 @@ +{ + "name": "@backstage/plugin-search-backend-module-stack-overflow", + "description": "A module for the search backend that exports stack overflow modules", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/search-backend-module-stack-overflow" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-search-backend-node": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "node-fetch": "^2.6.7", + "qs": "^6.9.4", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "msw": "^1.2.1" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/search-backend-module-stack-overflow/src/alpha.test.ts b/plugins/search-backend-module-stack-overflow/src/alpha.test.ts new file mode 100644 index 0000000000..1dba1ab8c0 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/src/alpha.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; +import searchModuleStackOverflowCollator from './alpha'; + +describe('searchModuleStackOverflowCollator', () => { + const schedule = { + frequency: { minutes: 10 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 3 }, + }; + + it('should register the stack overflow collator to the search index registry extension point with factory and schedule', async () => { + const extensionPointMock = { + addCollator: jest.fn(), + }; + + await startTestBackend({ + extensionPoints: [ + [searchIndexRegistryExtensionPoint, extensionPointMock], + ], + features: [ + searchModuleStackOverflowCollator(), + mockServices.rootConfig.factory({ + data: { + stackoverflow: { + schedule, + }, + }, + }), + ], + }); + + expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1); + expect(extensionPointMock.addCollator).toHaveBeenCalledWith({ + factory: expect.objectContaining({ type: 'stack-overflow' }), + schedule: expect.objectContaining({ run: expect.any(Function) }), + }); + }); +}); diff --git a/plugins/stack-overflow-backend/src/alpha.ts b/plugins/search-backend-module-stack-overflow/src/alpha.ts similarity index 90% rename from plugins/stack-overflow-backend/src/alpha.ts rename to plugins/search-backend-module-stack-overflow/src/alpha.ts index 897cbd1ea2..85b686c9ed 100644 --- a/plugins/stack-overflow-backend/src/alpha.ts +++ b/plugins/search-backend-module-stack-overflow/src/alpha.ts @@ -21,7 +21,7 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { StackOverflowQuestionsCollatorFactory } from './search'; +import { StackOverflowQuestionsCollatorFactory } from './collators'; /** * @packageDocumentation @@ -52,9 +52,9 @@ export default createBackendModule({ initialDelay: { seconds: 3 }, }; - const schedule = config.has('search.collators.stackoverflow.schedule') + const schedule = config.has('stackoverflow.schedule') ? readTaskScheduleDefinitionFromConfig( - config.getConfig('search.collators.stackoverflow.schedule'), + config.getConfig('stackoverflow.schedule'), ) : defaultSchedule; diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts similarity index 100% rename from plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.test.ts rename to plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts similarity index 100% rename from plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts rename to plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/index.ts b/plugins/search-backend-module-stack-overflow/src/collators/index.ts new file mode 100644 index 0000000000..1170b52423 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow/src/collators/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 StackOverflowDocument, + type StackOverflowQuestionsRequestParams, + type StackOverflowQuestionsCollatorFactoryOptions, + StackOverflowQuestionsCollatorFactory, +} from './StackOverflowQuestionsCollatorFactory'; diff --git a/plugins/stack-overflow-backend/src/search/index.ts b/plugins/search-backend-module-stack-overflow/src/index.ts similarity index 76% rename from plugins/stack-overflow-backend/src/search/index.ts rename to plugins/search-backend-module-stack-overflow/src/index.ts index ed3e05cb28..eb1f7540de 100644 --- a/plugins/stack-overflow-backend/src/search/index.ts +++ b/plugins/search-backend-module-stack-overflow/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2023 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. @@ -14,4 +14,9 @@ * limitations under the License. */ -export * from './StackOverflowQuestionsCollatorFactory'; +/** + * @packageDocumentation + * A module for the search backend that exports Stack Overflow modules. + */ + +export * from './collators'; diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md index e8fd710cf6..d113f7a0b6 100644 --- a/plugins/stack-overflow-backend/api-report.md +++ b/plugins/stack-overflow-backend/api-report.md @@ -3,54 +3,21 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// +import { StackOverflowDocument as StackOverflowDocument_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; +import { StackOverflowQuestionsCollatorFactory as StackOverflowQuestionsCollatorFactory_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; +import { StackOverflowQuestionsRequestParams as StackOverflowQuestionsRequestParams_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; -import { Config } from '@backstage/config'; -import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; -import { IndexableDocument } from '@backstage/plugin-search-common'; -import { Logger } from 'winston'; -import { Readable } from 'stream'; +// @public @deprecated (undocumented) +export type StackOverflowDocument = StackOverflowDocument_2; -// @public -export interface StackOverflowDocument extends IndexableDocument { - // (undocumented) - answers: number; - // (undocumented) - tags: string[]; -} +// @public @deprecated (undocumented) +export const StackOverflowQuestionsCollatorFactory: typeof StackOverflowQuestionsCollatorFactory_2; -// @public -export class StackOverflowQuestionsCollatorFactory - implements DocumentCollatorFactory -{ - // (undocumented) - execute(): AsyncGenerator; - // (undocumented) - static fromConfig( - config: Config, - options: StackOverflowQuestionsCollatorFactoryOptions, - ): StackOverflowQuestionsCollatorFactory; - // (undocumented) - getCollator(): Promise; - // (undocumented) - protected requestParams: StackOverflowQuestionsRequestParams; - // (undocumented) - readonly type: string; -} +// @public @deprecated (undocumented) +export type StackOverflowQuestionsCollatorFactoryOptions = + StackOverflowQuestionsCollatorFactory_2; -// @public -export type StackOverflowQuestionsCollatorFactoryOptions = { - baseUrl?: string; - maxPage?: number; - apiKey?: string; - apiAccessToken?: string; - teamName?: string; - requestParams?: StackOverflowQuestionsRequestParams; - logger: Logger; -}; - -// @public -export type StackOverflowQuestionsRequestParams = { - [key: string]: string | string[] | number; -}; +// @public @deprecated (undocumented) +export type StackOverflowQuestionsRequestParams = + StackOverflowQuestionsRequestParams_2; ``` diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 53857082d2..5235c33f1a 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -5,22 +5,9 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public" - }, - "exports": { - ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "alpha": [ - "src/alpha.ts" - ], - "package.json": [ - "package.json" - ] - } + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" }, "backstage": { "role": "backend-plugin" @@ -46,10 +33,8 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", - "@backstage/backend-plugin-api": "workspace:^", - "@backstage/backend-tasks": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/plugin-search-backend-node": "workspace:^", + "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "node-fetch": "^2.6.7", "qs": "^6.9.4", @@ -58,6 +43,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-search-backend-node": "workspace:^", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts index 6e461b8183..269748424b 100644 --- a/plugins/stack-overflow-backend/src/index.ts +++ b/plugins/stack-overflow-backend/src/index.ts @@ -20,4 +20,40 @@ * @packageDocumentation */ -export * from './search'; +import { + StackOverflowDocument as _StackOverflowDocument, + StackOverflowQuestionsRequestParams as _StackOverflowQuestionsRequestParams, + StackOverflowQuestionsCollatorFactory as _StackOverflowQuestionsCollatorFactory, + StackOverflowQuestionsCollatorFactoryOptions as _StackOverflowQuestionsCollatorFactoryOptions, +} from '@backstage/plugin-search-backend-module-stack-overflow'; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export type StackOverflowDocument = _StackOverflowDocument; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export type StackOverflowQuestionsRequestParams = + _StackOverflowQuestionsRequestParams; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export type StackOverflowQuestionsCollatorFactoryOptions = + _StackOverflowQuestionsCollatorFactory; + +/** + * @public + * @deprecated + * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + */ +export const StackOverflowQuestionsCollatorFactory = + _StackOverflowQuestionsCollatorFactory; diff --git a/yarn.lock b/yarn.lock index bc7aa10d86..6e2db68238 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8847,6 +8847,25 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-search-backend-module-stack-overflow@workspace:^, @backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow": + version: 0.0.0-use.local + resolution: "@backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-search-backend-node": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + msw: ^1.2.1 + node-fetch: ^2.6.7 + qs: ^6.9.4 + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/plugin-search-backend-module-techdocs@workspace:^, @backstage/plugin-search-backend-module-techdocs@workspace:plugins/search-backend-module-techdocs": version: 0.0.0-use.local resolution: "@backstage/plugin-search-backend-module-techdocs@workspace:plugins/search-backend-module-techdocs" @@ -9182,16 +9201,15 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-stack-overflow-backend@workspace:^, @backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": +"@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-stack-overflow-backend@workspace:plugins/stack-overflow-backend" dependencies: "@backstage/backend-common": "workspace:^" - "@backstage/backend-plugin-api": "workspace:^" - "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" + "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.0.0 @@ -25317,7 +25335,6 @@ __metadata: "@backstage/plugin-search-react": "workspace:^" "@backstage/plugin-sentry": "workspace:^" "@backstage/plugin-shortcuts": "workspace:^" - "@backstage/plugin-stack-overflow": "workspace:^" "@backstage/plugin-stackstorm": "workspace:^" "@backstage/plugin-tech-insights": "workspace:^" "@backstage/plugin-tech-radar": "workspace:^" @@ -25505,7 +25522,6 @@ __metadata: "@backstage/plugin-search-backend-module-techdocs": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-sonarqube-backend": "workspace:^" - "@backstage/plugin-stack-overflow-backend": "workspace:^" "@backstage/plugin-techdocs-backend": "workspace:^" "@backstage/plugin-todo-backend": "workspace:^" languageName: unknown From 20f8768427927c70311301568ce6e608da50c8fc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 16:31:36 +0200 Subject: [PATCH 038/141] refactor(search): do not add stack overflow types on the default page Signed-off-by: Camila Belo --- plugins/search/src/alpha.tsx | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 913a96d094..14e7067dfa 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -69,25 +69,6 @@ import { SearchType } from './components/SearchType'; import { UrlUpdater } from './components/SearchPage/SearchPage'; import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -/** @internal */ -const StackOverflowIcon = () => { - return ( - - - - - ); -}; - /** @alpha */ export const SearchApi = createApiExtension({ factory: { @@ -172,11 +153,6 @@ export const SearchPage = createPageExtension({ name: 'Architecture Decision Records', icon: , }, - { - value: 'stack-overflow', - name: 'Stack Overflow', - icon: , - }, ]} /> From 0ea51599408c1664640b11a595009daf3a61ca15 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 16:55:34 +0200 Subject: [PATCH 039/141] fix: docs link to stack overflow collator Signed-off-by: Camila Belo --- docs/plugins/integrating-search-into-plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index 7fb8e40b20..f2692d4e91 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -22,7 +22,7 @@ Imagine you have a plugin that is responsible for storing FAQ snippets in a data The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. -> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. +> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. #### 1. Install collator interface dependencies From 46f0f1700eb803a416bd389cf9d713b2c41939c9 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 9 Oct 2023 17:15:15 +0200 Subject: [PATCH 040/141] fix: update changeset files Signed-off-by: Camila Belo --- .changeset/chilly-terms-behave.md | 5 +++++ .changeset/giant-cycles-end.md | 4 ++-- .changeset/mean-fans-cough.md | 5 ----- .changeset/nice-pillows-poke.md | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 .changeset/chilly-terms-behave.md delete mode 100644 .changeset/mean-fans-cough.md diff --git a/.changeset/chilly-terms-behave.md b/.changeset/chilly-terms-behave.md new file mode 100644 index 0000000000..47250364bf --- /dev/null +++ b/.changeset/chilly-terms-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-stack-overflow': patch +--- + +Extract a package for the Stack Overflow new backend system plugin. diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md index 5d9f8149a8..6f637e05dd 100644 --- a/.changeset/giant-cycles-end.md +++ b/.changeset/giant-cycles-end.md @@ -2,6 +2,6 @@ '@backstage/plugin-stack-overflow-backend': patch --- -Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. +Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow` module. -The search collator `requestParams` option is optional now, so its defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). +The search collator `requestParams` option is optional now, so its default value is `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as defined in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). diff --git a/.changeset/mean-fans-cough.md b/.changeset/mean-fans-cough.md deleted file mode 100644 index 8f93cd361c..0000000000 --- a/.changeset/mean-fans-cough.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-search': patch ---- - -Add Stack Overflow type to the default alpha search page extension types filter. diff --git a/.changeset/nice-pillows-poke.md b/.changeset/nice-pillows-poke.md index 1a3926f371..64034c39d2 100644 --- a/.changeset/nice-pillows-poke.md +++ b/.changeset/nice-pillows-poke.md @@ -2,4 +2,4 @@ '@backstage/plugin-stack-overflow': patch --- -Migrate package to the new Backend system, the new module is distributed with a `/alpha` subpath. +Migrate package to the new Frontend system, the new module is distributed with a `/alpha` subpath. From 0eaa698bb5a73a7f13b6bd4d1b51af84068d40f4 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 09:38:33 +0200 Subject: [PATCH 041/141] chore(frontend-plugin-api): install frontend app api Signed-off-by: Camila Belo --- packages/frontend-plugin-api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index baf8cb4575..9cc3355ec3 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -24,7 +24,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@backstage/frontend-app-api": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", @@ -39,6 +38,7 @@ }, "dependencies": { "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-app-api": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "@types/react": "^16.13.1 || ^17.0.0", From 67cc85bb1465e342734a5cb877183849f7254a12 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 10:53:42 +0200 Subject: [PATCH 042/141] dev-utils,techdocs: use dynamic import instead of require for conditional react-dom Signed-off-by: Patrik Oldsberg --- .changeset/quick-roses-move.md | 6 +++++ packages/dev-utils/src/devApp/render.tsx | 22 ++++++++++++------- .../reader/transformers/renderReactElement.ts | 20 ++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 .changeset/quick-roses-move.md diff --git a/.changeset/quick-roses-move.md b/.changeset/quick-roses-move.md new file mode 100644 index 0000000000..767ad0919e --- /dev/null +++ b/.changeset/quick-roses-move.md @@ -0,0 +1,6 @@ +--- +'@backstage/dev-utils': patch +'@backstage/plugin-techdocs': patch +--- + +Switched the conditional `react-dom/client` import to use `import(...)` rather than `require(...)`. diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index e45ba649f9..ccb1cca2e8 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -49,11 +49,13 @@ import { createRoutesFromChildren, Route } from 'react-router-dom'; import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; import 'react-dom'; -let ReactDOM: typeof import('react-dom') | typeof import('react-dom/client'); +let ReactDOMPromise: Promise< + typeof import('react-dom') | typeof import('react-dom/client') +>; if (process.env.HAS_REACT_DOM_CLIENT) { - ReactDOM = require('react-dom/client'); + ReactDOMPromise = import('react-dom/client'); } else { - ReactDOM = require('react-dom'); + ReactDOMPromise = import('react-dom'); } export function isReactRouterBeta(): boolean { @@ -242,11 +244,15 @@ export class DevAppBuilder { window.location.pathname = this.defaultPage; } - if ('createRoot' in ReactDOM) { - ReactDOM.createRoot(document.getElementById('root')!).render(); - } else { - ReactDOM.render(, document.getElementById('root')); - } + ReactDOMPromise.then(ReactDOM => { + if ('createRoot' in ReactDOM) { + ReactDOM.createRoot(document.getElementById('root')!).render( + , + ); + } else { + ReactDOM.render(, document.getElementById('root')); + } + }); } } diff --git a/plugins/techdocs/src/reader/transformers/renderReactElement.ts b/plugins/techdocs/src/reader/transformers/renderReactElement.ts index f9b45065f1..ff7accf6ba 100644 --- a/plugins/techdocs/src/reader/transformers/renderReactElement.ts +++ b/plugins/techdocs/src/reader/transformers/renderReactElement.ts @@ -14,18 +14,22 @@ * limitations under the License. */ -let ReactDOM: typeof import('react-dom') | typeof import('react-dom/client'); +let ReactDOMPromise: Promise< + typeof import('react-dom') | typeof import('react-dom/client') +>; if (process.env.HAS_REACT_DOM_CLIENT) { - ReactDOM = require('react-dom/client'); + ReactDOMPromise = import('react-dom/client'); } else { - ReactDOM = require('react-dom'); + ReactDOMPromise = import('react-dom'); } /** @internal */ export function renderReactElement(element: JSX.Element, root: HTMLElement) { - if ('createRoot' in ReactDOM) { - ReactDOM.createRoot(root).render(element); - } else { - ReactDOM.render(element, root); - } + ReactDOMPromise.then(ReactDOM => { + if ('createRoot' in ReactDOM) { + ReactDOM.createRoot(root).render(element); + } else { + ReactDOM.render(element, root); + } + }); } From 3c39045e2bcee481ae410e6115b8abbc449147dc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 12:08:57 +0200 Subject: [PATCH 043/141] refactor: rename stack overflow backend module Signed-off-by: Camila Belo --- .changeset/chilly-terms-behave.md | 2 +- .changeset/giant-cycles-end.md | 2 +- .../integrating-search-into-plugins.md | 2 +- .../.eslintrc.js | 0 .../README.md | 97 +++++++++++++++++++ .../alpha-api-report.md | 2 +- .../api-report.md | 2 +- .../catalog-info.yaml | 4 +- .../config.d.ts | 0 .../package.json | 2 +- .../src/alpha.test.ts | 0 .../src/alpha.ts | 0 ...ckOverflowQuestionsCollatorFactory.test.ts | 0 .../StackOverflowQuestionsCollatorFactory.ts | 0 .../src/collators/index.ts | 0 .../src/index.ts | 0 .../README.md | 30 ------ plugins/stack-overflow-backend/api-report.md | 6 +- plugins/stack-overflow-backend/package.json | 2 +- plugins/stack-overflow-backend/src/index.ts | 10 +- yarn.lock | 6 +- 21 files changed, 117 insertions(+), 50 deletions(-) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/.eslintrc.js (100%) create mode 100644 plugins/search-backend-module-stack-overflow-collator/README.md rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/alpha-api-report.md (92%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/api-report.md (98%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/catalog-info.yaml (79%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/config.d.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/package.json (99%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/alpha.test.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/alpha.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/collators/StackOverflowQuestionsCollatorFactory.test.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/collators/StackOverflowQuestionsCollatorFactory.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/collators/index.ts (100%) rename plugins/{search-backend-module-stack-overflow => search-backend-module-stack-overflow-collator}/src/index.ts (100%) delete mode 100644 plugins/search-backend-module-stack-overflow/README.md diff --git a/.changeset/chilly-terms-behave.md b/.changeset/chilly-terms-behave.md index 47250364bf..b514bc45c8 100644 --- a/.changeset/chilly-terms-behave.md +++ b/.changeset/chilly-terms-behave.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-search-backend-module-stack-overflow': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': patch --- Extract a package for the Stack Overflow new backend system plugin. diff --git a/.changeset/giant-cycles-end.md b/.changeset/giant-cycles-end.md index 6f637e05dd..f69938ea25 100644 --- a/.changeset/giant-cycles-end.md +++ b/.changeset/giant-cycles-end.md @@ -2,6 +2,6 @@ '@backstage/plugin-stack-overflow-backend': patch --- -Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow` module. +Deprecate package in favor of the new `@backstage/plugin-search-backend-module-stack-overflow-collator` module. The search collator `requestParams` option is optional now, so its default value is `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as defined in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). diff --git a/docs/plugins/integrating-search-into-plugins.md b/docs/plugins/integrating-search-into-plugins.md index f2692d4e91..28c8b410e3 100644 --- a/docs/plugins/integrating-search-into-plugins.md +++ b/docs/plugins/integrating-search-into-plugins.md @@ -22,7 +22,7 @@ Imagine you have a plugin that is responsible for storing FAQ snippets in a data The search platform provides an interface (`DocumentCollatorFactory` from package `@backstage/plugin-search-common`) that allows you to do exactly that. It works by registering each of your entries as a "document" that later represents one search result each. -> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. +> You can always look at a working example, e.g. [StackOverflowQuestionsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts), if you are unsure or want to follow best practices. #### 1. Install collator interface dependencies diff --git a/plugins/search-backend-module-stack-overflow/.eslintrc.js b/plugins/search-backend-module-stack-overflow-collator/.eslintrc.js similarity index 100% rename from plugins/search-backend-module-stack-overflow/.eslintrc.js rename to plugins/search-backend-module-stack-overflow-collator/.eslintrc.js diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md new file mode 100644 index 0000000000..914746cbf9 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow-collator/README.md @@ -0,0 +1,97 @@ +# Stack Overflow Search Backend Module + +A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App. + +## Getting started + +Before we begin, make sure: + +- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/). + +To use any of the functionality this plugin provides, you need to start by configuring your App with the following config: + +```yaml +stackoverflow: + baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance +``` + +### Stack Overflow for Teams + +If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key or Personal Access Token. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api). + +The existing API key approach remains the default, to support the new v2.3 API and PAT authentication model you need to pass the team name and the new PAT into the existing apiAccessToken parameter to the new URL. See [15770](https://github.com/backstage/backstage/issues/15770) for more details. + +```yaml +stackoverflow: + baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance + apiKey: $STACK_OVERFLOW_API_KEY + apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN +``` + +```yaml +stackoverflow: + baseUrl: https://api.stackoverflowteams.com/2.3 # alternative: your internal stack overflow instance + teamName: $STACK_OVERFLOW_TEAM_NAME + apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN +``` + +## Areas of Responsibility + +This stack overflow backend plugin is primarily responsible for the following: + +- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search. + +### Index Stack Overflow Questions to search + +Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). + +When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). + +> Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions) + +```ts +indexBuilder.addCollator({ + schedule, + factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, { + logger: env.logger, + requestParams: { + tagged: ['backstage'], + site: 'stackoverflow', + pagesize: 100, + }, + }), +}); +``` + +## New Backend System + +> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. + +This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions). + +### Installation + +Add the module package as a dependency: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow-collator +``` + +Add the collator to your backend instance, along with the search plugin itself: + +```tsx +// packages/backend/src/index.ts +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); +backend.add(import('@backstage/plugin-search-backend/alpha')); +backend.add( + import( + '@backstage/plugin-search-backend-module-stack-overflow-collator/alpha' + ), +); +backend.start(); +``` + +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. diff --git a/plugins/search-backend-module-stack-overflow/alpha-api-report.md b/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md similarity index 92% rename from plugins/search-backend-module-stack-overflow/alpha-api-report.md rename to plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md index 9d25cb0a7d..a77d5e9598 100644 --- a/plugins/search-backend-module-stack-overflow/alpha-api-report.md +++ b/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow-collator" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/search-backend-module-stack-overflow/api-report.md b/plugins/search-backend-module-stack-overflow-collator/api-report.md similarity index 98% rename from plugins/search-backend-module-stack-overflow/api-report.md rename to plugins/search-backend-module-stack-overflow-collator/api-report.md index 795b30cac2..996260c4aa 100644 --- a/plugins/search-backend-module-stack-overflow/api-report.md +++ b/plugins/search-backend-module-stack-overflow-collator/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/plugin-search-backend-module-stack-overflow" +## API Report File for "@backstage/plugin-search-backend-module-stack-overflow-collator" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/search-backend-module-stack-overflow/catalog-info.yaml b/plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml similarity index 79% rename from plugins/search-backend-module-stack-overflow/catalog-info.yaml rename to plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml index 9075d1d591..ed73bd0eca 100644 --- a/plugins/search-backend-module-stack-overflow/catalog-info.yaml +++ b/plugins/search-backend-module-stack-overflow-collator/catalog-info.yaml @@ -1,8 +1,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: backstage-plugin-search-backend-module-stack-overflow - title: '@backstage/plugin-search-backend-module-stack-overflow' + name: backstage-plugin-search-backend-module-stack-overflow-collator + title: '@backstage/plugin-search-backend-module-stack-overflow-collator' description: A module for the search backend that exports stack overflow modules spec: lifecycle: experimental diff --git a/plugins/search-backend-module-stack-overflow/config.d.ts b/plugins/search-backend-module-stack-overflow-collator/config.d.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/config.d.ts rename to plugins/search-backend-module-stack-overflow-collator/config.d.ts diff --git a/plugins/search-backend-module-stack-overflow/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json similarity index 99% rename from plugins/search-backend-module-stack-overflow/package.json rename to plugins/search-backend-module-stack-overflow-collator/package.json index 64ff4d32d3..6e0b484e40 100644 --- a/plugins/search-backend-module-stack-overflow/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/plugin-search-backend-module-stack-overflow", + "name": "@backstage/plugin-search-backend-module-stack-overflow-collator", "description": "A module for the search backend that exports stack overflow modules", "version": "0.0.0", "main": "src/index.ts", diff --git a/plugins/search-backend-module-stack-overflow/src/alpha.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/alpha.test.ts rename to plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts diff --git a/plugins/search-backend-module-stack-overflow/src/alpha.ts b/plugins/search-backend-module-stack-overflow-collator/src/alpha.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/alpha.ts rename to plugins/search-backend-module-stack-overflow-collator/src/alpha.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.test.ts rename to plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.test.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/collators/StackOverflowQuestionsCollatorFactory.ts rename to plugins/search-backend-module-stack-overflow-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts diff --git a/plugins/search-backend-module-stack-overflow/src/collators/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/collators/index.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/collators/index.ts rename to plugins/search-backend-module-stack-overflow-collator/src/collators/index.ts diff --git a/plugins/search-backend-module-stack-overflow/src/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/index.ts similarity index 100% rename from plugins/search-backend-module-stack-overflow/src/index.ts rename to plugins/search-backend-module-stack-overflow-collator/src/index.ts diff --git a/plugins/search-backend-module-stack-overflow/README.md b/plugins/search-backend-module-stack-overflow/README.md deleted file mode 100644 index cba9617ea8..0000000000 --- a/plugins/search-backend-module-stack-overflow/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# search-backend-module-stack-overflow - -> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below. - -This package exports a module that extends the search backend to also indexing the questions exposed by the [`Stack Overflow` API](https://api.stackexchange.com/docs/questions). - -## Installation - -Add the module package as a dependency: - -```bash -# From your Backstage root directory -yarn add --cwd packages/backend @backstage/plugin-search-backend-module-stack-overflow -``` - -Add the collator to your backend instance, along with the search plugin itself: - -```tsx -// packages/backend/src/index.ts -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); -backend.add(import('@backstage/plugin-search-backend/alpha')); -backend.add( - import('@backstage/plugin-search-backend-module-stack-overflow/alpha'), -); -backend.start(); -``` - -You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md index d113f7a0b6..14dcdf2007 100644 --- a/plugins/stack-overflow-backend/api-report.md +++ b/plugins/stack-overflow-backend/api-report.md @@ -3,9 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { StackOverflowDocument as StackOverflowDocument_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; -import { StackOverflowQuestionsCollatorFactory as StackOverflowQuestionsCollatorFactory_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; -import { StackOverflowQuestionsRequestParams as StackOverflowQuestionsRequestParams_2 } from '@backstage/plugin-search-backend-module-stack-overflow'; +import { StackOverflowDocument as StackOverflowDocument_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator'; +import { StackOverflowQuestionsCollatorFactory as StackOverflowQuestionsCollatorFactory_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator'; +import { StackOverflowQuestionsRequestParams as StackOverflowQuestionsRequestParams_2 } from '@backstage/plugin-search-backend-module-stack-overflow-collator'; // @public @deprecated (undocumented) export type StackOverflowDocument = StackOverflowDocument_2; diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 5235c33f1a..c7b65a5ef5 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -34,7 +34,7 @@ "dependencies": { "@backstage/backend-common": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "workspace:^", "@backstage/plugin-search-common": "workspace:^", "node-fetch": "^2.6.7", "qs": "^6.9.4", diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts index 269748424b..e676de15e2 100644 --- a/plugins/stack-overflow-backend/src/index.ts +++ b/plugins/stack-overflow-backend/src/index.ts @@ -25,19 +25,19 @@ import { StackOverflowQuestionsRequestParams as _StackOverflowQuestionsRequestParams, StackOverflowQuestionsCollatorFactory as _StackOverflowQuestionsCollatorFactory, StackOverflowQuestionsCollatorFactoryOptions as _StackOverflowQuestionsCollatorFactoryOptions, -} from '@backstage/plugin-search-backend-module-stack-overflow'; +} from '@backstage/plugin-search-backend-module-stack-overflow-collator'; /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export type StackOverflowDocument = _StackOverflowDocument; /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export type StackOverflowQuestionsRequestParams = _StackOverflowQuestionsRequestParams; @@ -45,7 +45,7 @@ export type StackOverflowQuestionsRequestParams = /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export type StackOverflowQuestionsCollatorFactoryOptions = _StackOverflowQuestionsCollatorFactory; @@ -53,7 +53,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = /** * @public * @deprecated - * Import from `@backstage/plugin-search-backend-module-stack-overflow` instead. + * Import from `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ export const StackOverflowQuestionsCollatorFactory = _StackOverflowQuestionsCollatorFactory; diff --git a/yarn.lock b/yarn.lock index 6e2db68238..1267ee2cb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8847,9 +8847,9 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-search-backend-module-stack-overflow@workspace:^, @backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow": +"@backstage/plugin-search-backend-module-stack-overflow-collator@workspace:^, @backstage/plugin-search-backend-module-stack-overflow-collator@workspace:plugins/search-backend-module-stack-overflow-collator": version: 0.0.0-use.local - resolution: "@backstage/plugin-search-backend-module-stack-overflow@workspace:plugins/search-backend-module-stack-overflow" + resolution: "@backstage/plugin-search-backend-module-stack-overflow-collator@workspace:plugins/search-backend-module-stack-overflow-collator" dependencies: "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" @@ -9209,7 +9209,7 @@ __metadata: "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" - "@backstage/plugin-search-backend-module-stack-overflow": "workspace:^" + "@backstage/plugin-search-backend-module-stack-overflow-collator": "workspace:^" "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.0.0 From 95dc40505ac8bd01ec8c223a0cde04151b6c6e3d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 12:51:32 +0200 Subject: [PATCH 044/141] docs(stack-overflow): update api reports Signed-off-by: Camila Belo --- plugins/stack-overflow/alpha-api-report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/stack-overflow/alpha-api-report.md b/plugins/stack-overflow/alpha-api-report.md index 84d7616759..8bf621b948 100644 --- a/plugins/stack-overflow/alpha-api-report.md +++ b/plugins/stack-overflow/alpha-api-report.md @@ -3,10 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyExternalRoutes } from '@backstage/core-plugin-api'; +import { AnyRoutes } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin; +const _default: BackstagePlugin; export default _default; // (No @packageDocumentation comment for this package) From 3b5bef78b297df7ce846b12f2ecac91e3a34036e Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 18 Oct 2023 10:51:04 +0200 Subject: [PATCH 045/141] chore(stack-overflow-backend): deprecate entire package Signed-off-by: Camila Belo --- .../README.md | 2 +- plugins/stack-overflow-backend/README.md | 65 +------------------ plugins/stack-overflow-backend/package.json | 1 + plugins/stack-overflow-backend/src/index.ts | 5 +- plugins/stack-overflow/alpha-api-report.md | 4 +- 5 files changed, 8 insertions(+), 69 deletions(-) diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md index 914746cbf9..5fba2a3c79 100644 --- a/plugins/search-backend-module-stack-overflow-collator/README.md +++ b/plugins/search-backend-module-stack-overflow-collator/README.md @@ -94,4 +94,4 @@ backend.add( backend.start(); ``` -You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow/config.d.ts) for more details. +You may also want to add configuration parameters to your app-config, for example for controlling the scheduled indexing interval. These parameters should be placed under the `stackoverflow` key. See [the config definition file](https://github.com/backstage/backstage/blob/master/plugins/search-backend-module-stack-overflow-collator/config.d.ts) for more details. diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md index 5de997faa6..8129d36e38 100644 --- a/plugins/stack-overflow-backend/README.md +++ b/plugins/stack-overflow-backend/README.md @@ -1,64 +1,3 @@ -# Stack Overflow +# Stack Overflow Backend -A plugin that provides stack overflow specific functionality that can be used in different ways (e.g. for search) to compose your Backstage App. - -## Getting started - -Before we begin, make sure: - -- You have created your own standalone Backstage app using @backstage/create-app and not using a fork of the backstage repository. If you haven't setup Backstage already, start [here](https://backstage.io/docs/getting-started/). - -To use any of the functionality this plugin provides, you need to start by configuring your App with the following config: - -```yaml -stackoverflow: - baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance -``` - -### Stack Overflow for Teams - -If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key or Personal Access Token. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api). - -The existing API key approach remains the default, to support the new v2.3 API and PAT authentication model you need to pass the team name and the new PAT into the existing apiAccessToken parameter to the new URL. See [15770](https://github.com/backstage/backstage/issues/15770) for more details. - -```yaml -stackoverflow: - baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance - apiKey: $STACK_OVERFLOW_API_KEY - apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN -``` - -```yaml -stackoverflow: - baseUrl: https://api.stackoverflowteams.com/2.3 # alternative: your internal stack overflow instance - teamName: $STACK_OVERFLOW_TEAM_NAME - apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN -``` - -## Areas of Responsibility - -This stack overflow backend plugin is primarily responsible for the following: - -- Provides a `StackOverflowQuestionsCollatorFactory`, which can be used in the search backend to index stack overflow questions to your Backstage Search. - -### Index Stack Overflow Questions to search - -Before you are able to start index stack overflow questions to search, you need to go through the [search getting started guide](https://backstage.io/docs/features/search/getting-started). - -When you have your `packages/backend/src/plugins/search.ts` file ready to make modifications, add the following code snippet to add the `StackOverflowQuestionsCollatorFactory`. Note that you can optionally modify the `requestParams`, otherwise it will defaults to `{ order: 'desc', sort: 'activity', site: 'stackoverflow' }` as done in the `Try It` section on the [official Stack Overflow API documentation](https://api.stackexchange.com/docs/questions). - -> Note: if your `baseUrl` is set to the external stack overflow api `https://api.stackexchange.com/2.2`, you can find optional and required parameters under the official API documentation under [`Usage of /questions GET`](https://api.stackexchange.com/docs/questions) - -```ts -indexBuilder.addCollator({ - schedule, - factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, { - logger: env.logger, - requestParams: { - tagged: ['backstage'], - site: 'stackoverflow', - pagesize: 100, - }, - }), -}); -``` +Deprecated, consider using `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index c7b65a5ef5..77ebd8c69e 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -1,5 +1,6 @@ { "name": "@backstage/plugin-stack-overflow-backend", + "description": "Deprecated, consider using @backstage/plugin-search-backend-module-stack-overflow-collator instead", "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/stack-overflow-backend/src/index.ts b/plugins/stack-overflow-backend/src/index.ts index e676de15e2..5ca1c8d283 100644 --- a/plugins/stack-overflow-backend/src/index.ts +++ b/plugins/stack-overflow-backend/src/index.ts @@ -15,9 +15,10 @@ */ /** - * Stack Overflow backend plugin - * * @packageDocumentation + * Stack Overflow backend plugin + * @deprecated + * Deprecated, consider using `@backstage/plugin-search-backend-module-stack-overflow-collator` instead. */ import { diff --git a/plugins/stack-overflow/alpha-api-report.md b/plugins/stack-overflow/alpha-api-report.md index 8bf621b948..167f8e1736 100644 --- a/plugins/stack-overflow/alpha-api-report.md +++ b/plugins/stack-overflow/alpha-api-report.md @@ -3,12 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AnyExternalRoutes } from '@backstage/core-plugin-api'; -import { AnyRoutes } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin; +const _default: BackstagePlugin<{}, {}>; export default _default; // (No @packageDocumentation comment for this package) From d7a4e2225f7cadb61240cb44ff4ccef72f54f190 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 09:10:35 +0200 Subject: [PATCH 046/141] fix: changeset package version Signed-off-by: Camila Belo --- .changeset/chilly-terms-behave.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chilly-terms-behave.md b/.changeset/chilly-terms-behave.md index b514bc45c8..8ee1ad8375 100644 --- a/.changeset/chilly-terms-behave.md +++ b/.changeset/chilly-terms-behave.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-search-backend-module-stack-overflow-collator': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': minor --- Extract a package for the Stack Overflow new backend system plugin. From 8a7f1ba2a511973bf1f82dc408e4526c88208b5f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 09:11:33 +0200 Subject: [PATCH 047/141] docs(stack-overflow): add note about missing hompage card extension Signed-off-by: Camila Belo --- plugins/stack-overflow/src/alpha.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/stack-overflow/src/alpha.tsx b/plugins/stack-overflow/src/alpha.tsx index 18f0a0df1c..3708271ef1 100644 --- a/plugins/stack-overflow/src/alpha.tsx +++ b/plugins/stack-overflow/src/alpha.tsx @@ -44,5 +44,6 @@ const StackOverflowSearchResultListItem = createSearchResultListItemExtension({ /** @alpha */ export default createPlugin({ id: 'stack-overflow', + // TODO: Migrate homepage cards when the declarative homepage plugin supports them extensions: [StackOverflowApi, StackOverflowSearchResultListItem], }); From e13e43a12509f72ae8322cbab1dc1f665c8c45b5 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 09:28:38 +0200 Subject: [PATCH 048/141] refactor(stack-overflow-backend): remove alpha subpath Signed-off-by: Camila Belo --- .../README.md | 4 +--- .../alpha-api-report.md | 11 ----------- .../api-report.md | 5 +++++ .../package.json | 19 +++---------------- .../src/index.ts | 1 + ...SearchStackOverflowCollatorModule.test.ts} | 6 +++--- .../SearchStackOverflowCollatorModule.ts} | 14 ++++---------- .../src/module/index.ts | 17 +++++++++++++++++ 8 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md rename plugins/search-backend-module-stack-overflow-collator/src/{alpha.test.ts => module/SearchStackOverflowCollatorModule.test.ts} (90%) rename plugins/search-backend-module-stack-overflow-collator/src/{alpha.ts => module/SearchStackOverflowCollatorModule.ts} (88%) create mode 100644 plugins/search-backend-module-stack-overflow-collator/src/module/index.ts diff --git a/plugins/search-backend-module-stack-overflow-collator/README.md b/plugins/search-backend-module-stack-overflow-collator/README.md index 5fba2a3c79..7df3229eeb 100644 --- a/plugins/search-backend-module-stack-overflow-collator/README.md +++ b/plugins/search-backend-module-stack-overflow-collator/README.md @@ -87,9 +87,7 @@ import { createBackend } from '@backstage/backend-defaults'; const backend = createBackend(); backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add( - import( - '@backstage/plugin-search-backend-module-stack-overflow-collator/alpha' - ), + import('@backstage/plugin-search-backend-module-stack-overflow-collator'), ); backend.start(); ``` diff --git a/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md b/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md deleted file mode 100644 index a77d5e9598..0000000000 --- a/plugins/search-backend-module-stack-overflow-collator/alpha-api-report.md +++ /dev/null @@ -1,11 +0,0 @@ -## API Report File for "@backstage/plugin-search-backend-module-stack-overflow-collator" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -import { BackendFeature } from '@backstage/backend-plugin-api'; - -// @alpha -const _default: () => BackendFeature; -export default _default; -``` diff --git a/plugins/search-backend-module-stack-overflow-collator/api-report.md b/plugins/search-backend-module-stack-overflow-collator/api-report.md index 996260c4aa..9243e786ac 100644 --- a/plugins/search-backend-module-stack-overflow-collator/api-report.md +++ b/plugins/search-backend-module-stack-overflow-collator/api-report.md @@ -5,12 +5,17 @@ ```ts /// +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { Logger } from 'winston'; import { Readable } from 'stream'; +// @public +const searchStackOverflowCollatorModule: () => BackendFeature; +export default searchStackOverflowCollatorModule; + // @public export interface StackOverflowDocument extends IndexableDocument { // (undocumented) diff --git a/plugins/search-backend-module-stack-overflow-collator/package.json b/plugins/search-backend-module-stack-overflow-collator/package.json index 6e0b484e40..14e547de8e 100644 --- a/plugins/search-backend-module-stack-overflow-collator/package.json +++ b/plugins/search-backend-module-stack-overflow-collator/package.json @@ -6,22 +6,9 @@ "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { - "access": "public" - }, - "exports": { - ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", - "./package.json": "./package.json" - }, - "typesVersions": { - "*": { - "alpha": [ - "src/alpha.ts" - ], - "package.json": [ - "package.json" - ] - } + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" }, "backstage": { "role": "backend-plugin-module" diff --git a/plugins/search-backend-module-stack-overflow-collator/src/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/index.ts index eb1f7540de..00d57a7b27 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/index.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/index.ts @@ -20,3 +20,4 @@ */ export * from './collators'; +export { searchStackOverflowCollatorModule as default } from './module'; diff --git a/plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts similarity index 90% rename from plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts rename to plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts index 1dba1ab8c0..a785ead6eb 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/alpha.test.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.test.ts @@ -16,9 +16,9 @@ import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import searchModuleStackOverflowCollator from './alpha'; +import { searchStackOverflowCollatorModule } from './SearchStackOverflowCollatorModule'; -describe('searchModuleStackOverflowCollator', () => { +describe('searchStackOverflowCollatorModule', () => { const schedule = { frequency: { minutes: 10 }, timeout: { minutes: 15 }, @@ -35,7 +35,7 @@ describe('searchModuleStackOverflowCollator', () => { [searchIndexRegistryExtensionPoint, extensionPointMock], ], features: [ - searchModuleStackOverflowCollator(), + searchStackOverflowCollatorModule(), mockServices.rootConfig.factory({ data: { stackoverflow: { diff --git a/plugins/search-backend-module-stack-overflow-collator/src/alpha.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts similarity index 88% rename from plugins/search-backend-module-stack-overflow-collator/src/alpha.ts rename to plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts index 85b686c9ed..3ef3d72c32 100644 --- a/plugins/search-backend-module-stack-overflow-collator/src/alpha.ts +++ b/plugins/search-backend-module-stack-overflow-collator/src/module/SearchStackOverflowCollatorModule.ts @@ -21,20 +21,14 @@ import { createBackendModule, } from '@backstage/backend-plugin-api'; import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha'; -import { StackOverflowQuestionsCollatorFactory } from './collators'; - -/** - * @packageDocumentation - * A module for the search backend that exports Stack Overflow modules. - */ +import { StackOverflowQuestionsCollatorFactory } from '../collators'; /** + * @public * Search backend module for the Stack Overflow index. - * - * @alpha */ -export default createBackendModule({ - moduleId: 'stackoverflowCollator', +export const searchStackOverflowCollatorModule = createBackendModule({ + moduleId: 'stackOverflowCollator', pluginId: 'search', register(env) { env.registerInit({ diff --git a/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts b/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts new file mode 100644 index 0000000000..ab424398e0 --- /dev/null +++ b/plugins/search-backend-module-stack-overflow-collator/src/module/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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 { searchStackOverflowCollatorModule } from './SearchStackOverflowCollatorModule'; From ed71a018b1ece87f2e73e15eeac18a8555569308 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 12 Oct 2023 22:08:03 +0200 Subject: [PATCH 049/141] feat(frontend-plugin-api): improve extension boundary Signed-off-by: Camila Belo --- packages/frontend-plugin-api/package.json | 4 +- .../src/components/ErrorBoundary.tsx | 79 +++++++++++++++++++ .../src/components/ExtensionBoundary.tsx | 24 +++++- yarn.lock | 2 + 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 packages/frontend-plugin-api/src/components/ErrorBoundary.tsx diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index 9cc3355ec3..528d86e0d1 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@backstage/frontend-app-api": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", @@ -37,10 +38,11 @@ "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "dependencies": { + "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", - "@backstage/frontend-app-api": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", + "@material-ui/core": "^4.12.4", "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "zod": "^3.21.4", diff --git a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000000..f9ce9c9809 --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2023 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, { Component, PropsWithChildren } from 'react'; +import { Button } from '@material-ui/core'; +import { BackstagePlugin } from '../wiring'; +import { ErrorPanel } from '@backstage/core-components'; + +type DefaultErrorBoundaryFallbackProps = PropsWithChildren<{ + plugin?: BackstagePlugin; + error: Error; + resetError: () => void; +}>; + +const DefaultErrorBoundaryFallback = ({ + plugin, + error, + resetError, +}: DefaultErrorBoundaryFallbackProps) => { + const title = `Error in ${plugin?.id}`; + + return ( + + + + ); +}; + +type ErrorBoundaryProps = PropsWithChildren<{ plugin?: BackstagePlugin }>; +type ErrorBoundaryState = { error?: Error }; + +/** @internal */ +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + static getDerivedStateFromError(error: Error) { + return { error }; + } + + state: ErrorBoundaryState = { error: undefined }; + + handleErrorReset = () => { + this.setState({ error: undefined }); + }; + + render() { + const { error } = this.state; + const { plugin, children } = this.props; + + if (error) { + // TODO: use a configurable error boundary fallback + return ( + + ); + } + + return children; + } +} diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index c6a218c15f..796908c619 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -15,15 +15,35 @@ */ import React, { ReactNode } from 'react'; +import { AnalyticsContext } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '../wiring'; +import { RouteRef } from '../routing'; +import { ErrorBoundary } from './ErrorBoundary'; +import { toInternalRouteRef } from '../routing/RouteRef'; /** @public */ export interface ExtensionBoundaryProps { - children: ReactNode; + id: string; source?: BackstagePlugin; + routeRef?: RouteRef; + children: ReactNode; } /** @public */ export function ExtensionBoundary(props: ExtensionBoundaryProps) { - return <>{props.children}; + const { id, source, routeRef, children } = props; + + const attributes = { + extension: id, + pluginId: source?.id, + routeRef: routeRef + ? toInternalRouteRef(routeRef).getDescription() + : undefined, + }; + + return ( + + {children} + + ); } diff --git a/yarn.lock b/yarn.lock index 1e2f6fdb5d..8999966568 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4246,11 +4246,13 @@ __metadata: resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" dependencies: "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@backstage/version-bridge": "workspace:^" + "@material-ui/core": ^4.12.4 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 From b2dcb92c36e8c142f1b9bb386505b31368a3cfc7 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 15:57:50 +0200 Subject: [PATCH 050/141] test(frontend-plugin-api): cover extension boundary component Signed-off-by: Camila Belo --- .../src/components/ExtensionBoundary.test.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx new file mode 100644 index 0000000000..150ace717c --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright 2023 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, { useEffect } from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { + MockAnalyticsApi, + MockConfigApi, + TestApiProvider, + renderWithEffects, +} from '@backstage/test-utils'; +import { ExtensionBoundary } from './ExtensionBoundary'; +import { + Extension, + coreExtensionData, + createExtension, + createPlugin, +} from '../wiring'; +import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; +import { createApp } from '@backstage/frontend-app-api'; +import { JsonObject } from '@backstage/types'; +import { createRouteRef } from '../routing'; +import { toInternalRouteRef } from '../routing/RouteRef'; + +function renderExtensionInTestApp( + extension: Extension, + options?: { + config?: JsonObject; + }, +) { + const { config = {} } = options ?? {}; + + const app = createApp({ + features: [ + createPlugin({ + id: 'plugin', + extensions: [extension], + }), + ], + configLoader: async () => new MockConfigApi(config), + }); + + return renderWithEffects(app.createRoot()); +} + +const wrapInBoundaryExtension = (element: JSX.Element) => { + const id = 'plugin.extension'; + const routeRef = createRouteRef(); + toInternalRouteRef(routeRef).setId(id); + return createExtension({ + id, + attachTo: { id: 'core.routes', input: 'routes' }, + output: { + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + }, + factory({ bind, source }) { + bind({ + routeRef, + path: '/', + element: ( + + {element} + + ), + }); + }, + }); +}; + +describe('ExtensionBoundary', () => { + it('should render children when there is no error', async () => { + const text = 'Text Component'; + const TextComponent = () => { + return

{text}

; + }; + await renderExtensionInTestApp(wrapInBoundaryExtension()); + await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument()); + }); + + it('should show app error component when an error is thrown', async () => { + const error = 'Something went wrong'; + const ErrorComponent = () => { + throw new Error(error); + }; + await renderExtensionInTestApp(wrapInBoundaryExtension()); + await waitFor(() => expect(screen.getByText(error)).toBeInTheDocument()); + }); + + it('should wrap children with analytics context', async () => { + const action = 'render'; + const subject = 'analytics'; + const analyticsApiMock = new MockAnalyticsApi(); + + const AnalyticsComponent = () => { + const analytics = useAnalytics(); + useEffect(() => { + analytics.captureEvent(action, subject); + }, [analytics]); + return null; + }; + + await renderExtensionInTestApp( + wrapInBoundaryExtension( + + + , + ), + ); + + await waitFor(() => + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action, + subject, + context: { + extension: 'plugin.extension', + routeRef: 'plugin.extension', + }, + }), + ); + }); +}); From 2610774e4b281d5239788fced55ead61104ef02c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 12 Oct 2023 22:08:32 +0200 Subject: [PATCH 051/141] feat(frontend-plugin-api): create extension suspense Signed-off-by: Camila Belo --- .../src/components/ExtensionSuspense.tsx | 33 +++++++++++++++++++ .../src/components/index.ts | 5 +++ 2 files changed, 38 insertions(+) create mode 100644 packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx diff --git a/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx b/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx new file mode 100644 index 0000000000..e80f59f09c --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionSuspense.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2023 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, { ReactNode, Suspense } from 'react'; +import { useApp } from '@backstage/core-plugin-api'; + +/** @public */ +export interface ExtensionSuspenseProps { + children: ReactNode; +} + +/** @public */ +export function ExtensionSuspense(props: ExtensionSuspenseProps) { + const { children } = props; + + const app = useApp(); + const { Progress } = app.getComponents(); + + return }>{children}; +} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 7056a59271..fecd7f36cc 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -18,3 +18,8 @@ export { ExtensionBoundary, type ExtensionBoundaryProps, } from './ExtensionBoundary'; + +export { + ExtensionSuspense, + type ExtensionSuspenseProps, +} from './ExtensionSuspense'; From 055b7aec3da8b0b04801525d8180d47ea8978d36 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 10:56:10 +0200 Subject: [PATCH 052/141] test(frontend-plugin-api): cover extension suspense component Signed-off-by: Camila Belo --- .../src/components/ExtensionSuspense.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx diff --git a/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx new file mode 100644 index 0000000000..166c57b83b --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionSuspense.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2023 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, { lazy } from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { ExtensionSuspense } from './ExtensionSuspense'; + +describe('ExtensionSuspense', () => { + it('should render the app progress component as fallback', async () => { + const LazyComponent = lazy(() => new Promise(() => {})); + + await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(screen.getByTestId('progress')).toBeInTheDocument(); + }); + + it('should render the lazy loaded children component', async () => { + const LazyComponent = lazy(() => + Promise.resolve({ default: () =>
Lazy Component
}), + ); + + await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + await waitFor(() => + expect(screen.getByText('Lazy Component')).toBeInTheDocument(), + ); + }); +}); From 103b405746ada473d44cf9c01738c65010238592 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 10:05:37 +0200 Subject: [PATCH 053/141] feat(frontend-plugin-api): use extension boundary and suspense Signed-off-by: Camila Belo --- .../src/extensions/createPageExtension.tsx | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 217fb33ce5..ad7db1d222 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -14,18 +14,21 @@ * limitations under the License. */ -import React from 'react'; -import { ExtensionBoundary } from '../components'; +import React, { lazy, useEffect } from 'react'; +import { useAnalytics } from '@backstage/core-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; +import { ExtensionBoundary, ExtensionSuspense } from '../components'; import { createSchemaFromZod, PortableSchema } from '../schema'; import { coreExtensionData, createExtension, Extension, ExtensionInputValues, + AnyExtensionInputMap, } from '../wiring'; -import { AnyExtensionInputMap } from '../wiring/createExtension'; -import { Expand } from '../types'; import { RouteRef } from '../routing'; +import { Expand } from '../types'; /** * Helper for creating extensions for a routable React page component. @@ -55,6 +58,10 @@ export function createPageExtension< }) => Promise; }, ): Extension { + const { id, routeRef } = options; + + const attachTo = options.attachTo ?? { id: 'core.routes', input: 'routes' }; + const configSchema = 'configSchema' in options ? options.configSchema @@ -63,33 +70,49 @@ export function createPageExtension< ) as PortableSchema); return createExtension({ - id: options.id, - attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' }, + id, + attachTo, + configSchema, + inputs: options.inputs, disabled: options.disabled, output: { element: coreExtensionData.reactElement, path: coreExtensionData.routePath, routeRef: coreExtensionData.routeRef.optional(), }, - inputs: options.inputs, - configSchema, factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => + const { path } = config; + + const PageComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), ); + const ExtensionComponent = () => { + const analytics = useAnalytics(); + + // This event, never exposed to end-users of the analytics API, + // helps inform which extension metadata gets associated with a + // navigation event when the route navigated to is a gathered + // mountpoint. + useEffect(() => { + analytics.captureEvent(routableExtensionRenderedEvent, ''); + }, [analytics]); + + return ; + }; + bind({ - path: config.path, + path, + routeRef, element: ( - - - - + + + + ), - routeRef: options.routeRef, }); }, }); From 42a8ef3a4f2283de29d29f73eb3b94a6203f1d72 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 20:58:03 +0200 Subject: [PATCH 054/141] test(frontend-plugin-api): cover create page extension Signed-off-by: Camila Belo --- .../extensions/createPageExtension.test.tsx | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 125bf3495d..37d97e7621 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -15,10 +15,23 @@ */ import React from 'react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { useAnalytics } from '@backstage/core-plugin-api'; +import { waitFor } from '@testing-library/react'; import { PortableSchema } from '../schema'; -import { coreExtensionData, createExtensionInput } from '../wiring'; +import { + ExtensionInputValues, + coreExtensionData, + createExtensionInput, + createPlugin, +} from '../wiring'; import { createPageExtension } from './createPageExtension'; +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useAnalytics: jest.fn(), +})); + describe('createPageExtension', () => { it('creates the extension properly', () => { const configSchema: PortableSchema<{ path: string }> = { @@ -100,4 +113,35 @@ describe('createPageExtension', () => { factory: expect.any(Function), }); }); + + it('capture page view event in analytics', async () => { + const captureEvent = jest.fn(); + + (useAnalytics as jest.Mock).mockReturnValue({ + captureEvent, + }); + + const extension = createPageExtension({ + id: 'plugin.page', + defaultPath: '/', + loader: async () =>
Component
, + }); + + extension.factory({ + bind: (values: ExtensionInputValues) => + renderWithEffects( + wrapInTestApp(values.element as unknown as JSX.Element), + ), + source: createPlugin({ id: 'plugin ' }), + config: { path: '/' }, + inputs: {}, + }); + + await waitFor(() => + expect(captureEvent).toHaveBeenCalledWith( + '_ROUTABLE-EXTENSION-RENDERED', + '', + ), + ); + }); }); From c1e10b6175e59af190d005454096b36dc712527a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 09:23:51 +0200 Subject: [PATCH 055/141] docs(frontend-plugin-api): update api reports Signed-off-by: Camila Belo --- packages/frontend-plugin-api/api-report.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 3ed5965c99..b6f85e757b 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -338,6 +338,10 @@ export interface ExtensionBoundaryProps { // (undocumented) children: ReactNode; // (undocumented) + id: string; + // (undocumented) + routeRef?: RouteRef; + // (undocumented) source?: BackstagePlugin; } @@ -412,6 +416,17 @@ export interface ExtensionOverridesOptions { extensions: Extension[]; } +// @public (undocumented) +export function ExtensionSuspense( + props: ExtensionSuspenseProps, +): React_2.JSX.Element; + +// @public (undocumented) +export interface ExtensionSuspenseProps { + // (undocumented) + children: ReactNode; +} + // @public export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, From 6af88a05ff23e66d1c792e8fa6ba42fed4996c19 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 10:38:48 +0200 Subject: [PATCH 056/141] chore(frontend-plugin-api): add changeset file Signed-off-by: Camila Belo --- .changeset/sixty-tips-argue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sixty-tips-argue.md diff --git a/.changeset/sixty-tips-argue.md b/.changeset/sixty-tips-argue.md new file mode 100644 index 0000000000..9fc965a281 --- /dev/null +++ b/.changeset/sixty-tips-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Improve the extension boundary component and create a default extension suspense component. From b51919e3eb85f68916de6d930e079fa70ae8b55b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 13 Oct 2023 09:22:21 +0200 Subject: [PATCH 057/141] refactor(search-react): use extension boundary and suspense Signed-off-by: Camila Belo --- plugins/search-react/src/alpha.tsx | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index 366112c95e..f28bc44b0d 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -14,18 +14,18 @@ * limitations under the License. */ -import React, { lazy, Suspense } from 'react'; +import React, { lazy } from 'react'; import { ListItemProps } from '@material-ui/core'; import { ExtensionBoundary, + ExtensionSuspense, PortableSchema, createExtension, createExtensionDataRef, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; -import { Progress } from '@backstage/core-components'; import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; import { SearchResultListItemExtension } from './extensions'; @@ -87,6 +87,13 @@ export type SearchResultItemExtensionOptions< export function createSearchResultListItemExtension< TConfig extends { noTrack?: boolean }, >(options: SearchResultItemExtensionOptions) { + const id = `plugin.search.result.item.${options.id}`; + + const attachTo = options.attachTo ?? { + id: 'plugin.search.page', + input: 'items', + }; + const configSchema = 'configSchema' in options ? options.configSchema @@ -95,15 +102,16 @@ export function createSearchResultListItemExtension< noTrack: z.boolean().default(false), }), ) as PortableSchema); + return createExtension({ - id: `plugin.search.result.item.${options.id}`, - attachTo: options.attachTo ?? { id: 'plugin.search.page', input: 'items' }, + id, + attachTo, configSchema, output: { item: searchResultItemExtensionData, }, factory({ bind, config, source }) { - const LazyComponent = lazy(() => + const ExtensionComponent = lazy(() => options .component({ config }) .then(component => ({ default: component })), @@ -113,16 +121,16 @@ export function createSearchResultListItemExtension< item: { predicate: options.predicate, component: props => ( - - }> + + - + - + ), }, From e7c09c4f4b60d0107dcefe23a39ece2916b49ef3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 14 Oct 2023 10:39:27 +0200 Subject: [PATCH 058/141] chore(search-react): add changeset file Signed-off-by: Camila Belo --- .changeset/young-days-talk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/young-days-talk.md diff --git a/.changeset/young-days-talk.md b/.changeset/young-days-talk.md new file mode 100644 index 0000000000..b7420c7894 --- /dev/null +++ b/.changeset/young-days-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Use default extensions boundary and suspense on the alpha declarative `createSearchResultListItem` extension factory. From 11555a0da0153d52fbcd57dd94b8ff632539dbf9 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 11:32:39 +0200 Subject: [PATCH 059/141] refactor(catalog): forward plugin to extension boundary Signed-off-by: Camila Belo --- plugins/catalog/src/alpha.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 6eda8c3d26..27ec432d07 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -36,6 +36,7 @@ import { PortableSchema, ExtensionBoundary, createExtensionInput, + ExtensionSuspense, } from '@backstage/frontend-plugin-api'; import { AsyncEntityProvider, @@ -51,7 +52,6 @@ import { rootRouteRef, viewTechDocRouteRef, } from './routes'; -import { Progress } from '@backstage/core-components'; import { useEntityFromUrl } from './components/CatalogEntityPage/useEntityFromUrl'; /** @alpha */ @@ -97,16 +97,19 @@ export function createCatalogFilterExtension< configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; }) { + const id = `catalog.filter.${options.id}`; + const attachTo = { id: 'plugin.catalog.page.index', input: 'filters' }; + return createExtension({ - id: `catalog.filter.${options.id}`, - attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, + id, + attachTo, inputs: options.inputs ?? {}, configSchema: options.configSchema, output: { element: coreExtensionData.reactElement, }, factory({ bind, config, source }) { - const LazyComponent = React.lazy(() => + const ExtensionComponent = React.lazy(() => options .loader({ config }) .then(element => ({ default: () => element })), @@ -114,10 +117,10 @@ export function createCatalogFilterExtension< bind({ element: ( - - }> - - + + + + ), }); From e964c17db97ad0fdb27e9e2b2b6c19ad432e3e3a Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 11:49:47 +0200 Subject: [PATCH 060/141] chore(catalog): add changeset file Signed-off-by: Camila Belo --- .changeset/fluffy-years-shake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fluffy-years-shake.md diff --git a/.changeset/fluffy-years-shake.md b/.changeset/fluffy-years-shake.md new file mode 100644 index 0000000000..9612690d62 --- /dev/null +++ b/.changeset/fluffy-years-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Use default extensions boundary and suspense on the alpha declarative `createCatalogFilterExtension` extension factory. From 0fb1d8c7f8af8f53bc6aaf2928b78c8a836612ec Mon Sep 17 00:00:00 2001 From: Ankit Anand Date: Thu, 19 Oct 2023 16:46:23 +0530 Subject: [PATCH 061/141] removed mockdir.clear, replaced addContent with setContent Signed-off-by: Ankit Anand --- .../src/actions/fetch/rails/index.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts index 10b8466dc2..7548ddd21d 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.test.ts @@ -73,9 +73,6 @@ describe('fetch:rails', () => { createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; - mockDir.clear(); - mockDir.addContent({ template: {} }); - const mockReader: UrlReader = { readUrl: jest.fn(), readTree: jest.fn(), @@ -93,7 +90,7 @@ describe('fetch:rails', () => { }); beforeEach(() => { - mockDir.addContent({ + mockDir.setContent({ result: '{}', }); jest.clearAllMocks(); From c3c5c7e514f4c5145f176c575751a8023b88b006 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 19 Oct 2023 14:27:22 +0300 Subject: [PATCH 062/141] chore: add info about entity if tech docs building fails Signed-off-by: Heikki Hellgren --- .changeset/hip-mugs-camp.md | 5 +++++ .../src/service/DocsSynchronizer.test.ts | 2 +- .../techdocs-backend/src/service/DocsSynchronizer.ts | 10 ++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/hip-mugs-camp.md diff --git a/.changeset/hip-mugs-camp.md b/.changeset/hip-mugs-camp.md new file mode 100644 index 0000000000..3413b5ac0a --- /dev/null +++ b/.changeset/hip-mugs-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Add info about the entity when tech docs fail to build diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index 52a3b82651..ff714eee63 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -258,7 +258,7 @@ describe('DocsSynchronizer', () => { expect(mockResponseHandler.log).toHaveBeenCalledTimes(1); expect(mockResponseHandler.log).toHaveBeenCalledWith( expect.stringMatching( - /error.*: Failed to build the docs page: Some random error/, + /error.*: Failed to build the docs page for entity component:default\/test: Some random error/, ), ); expect(mockResponseHandler.finish).toHaveBeenCalledTimes(0); diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 8456f64a6a..fad915b068 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -15,7 +15,11 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { + DEFAULT_NAMESPACE, + Entity, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, NotFoundError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -142,7 +146,9 @@ export class DocsSynchronizer { } } catch (e) { assertError(e); - const msg = `Failed to build the docs page: ${e.message}`; + const msg = `Failed to build the docs page for entity ${stringifyEntityRef( + entity, + )}: ${e.message}`; taskLogger.error(msg); this.logger.error(msg, e); error(e); From 816493a7c50966b4e6c5491de754f764cc6ead37 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 13:55:47 +0200 Subject: [PATCH 063/141] docs: fix links to analytics-module-ga README Signed-off-by: Patrik Oldsberg --- docs/releases/v1.9.0-changelog.md | 2 +- docs/releases/v1.9.0-next.2-changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releases/v1.9.0-changelog.md b/docs/releases/v1.9.0-changelog.md index df14c3d34d..07451f6d2c 100644 --- a/docs/releases/v1.9.0-changelog.md +++ b/docs/releases/v1.9.0-changelog.md @@ -1299,7 +1299,7 @@ - d3fea4ae0a: Internal fixes to avoid implicit usage of globals - 3280711113: Updated dependency `msw` to `^0.49.0`. - 9516b0c355: Added support for sending virtual pageviews on `search` events in order to enable - Site Search functionality in GA. For more information consult [README](/plugins/analytics-module-ga/README.md#enabling-site-search) + Site Search functionality in GA. For more information consult [README](https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md#enabling-site-search) - Updated dependencies - @backstage/core-plugin-api@1.2.0 - @backstage/core-components@0.12.1 diff --git a/docs/releases/v1.9.0-next.2-changelog.md b/docs/releases/v1.9.0-next.2-changelog.md index a410da5263..2c6d495641 100644 --- a/docs/releases/v1.9.0-next.2-changelog.md +++ b/docs/releases/v1.9.0-next.2-changelog.md @@ -525,7 +525,7 @@ ### Patch Changes - 9516b0c355: Added support for sending virtual pageviews on `search` events in order to enable - Site Search functionality in GA. For more information consult [README](/plugins/analytics-module-ga/README.md#enabling-site-search) + Site Search functionality in GA. For more information consult [README](https://github.com/backstage/backstage/blob/master/plugins/analytics-module-ga/README.md#enabling-site-search) - Updated dependencies - @backstage/core-plugin-api@1.2.0-next.2 - @backstage/core-components@0.12.1-next.2 From 1b8f005aab095f83eb3652b049b1ab9192adfb2f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 13:57:35 +0200 Subject: [PATCH 064/141] refactor(catalog): restore inline attachment Signed-off-by: Camila Belo --- plugins/catalog/src/alpha.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 27ec432d07..e9cb083978 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -98,11 +98,10 @@ export function createCatalogFilterExtension< loader: (options: { config: TConfig }) => Promise; }) { const id = `catalog.filter.${options.id}`; - const attachTo = { id: 'plugin.catalog.page.index', input: 'filters' }; return createExtension({ id, - attachTo, + attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, inputs: options.inputs ?? {}, configSchema: options.configSchema, output: { From cca1afc547100a9412a75c77e3ef15cf0f04d691 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 13:57:54 +0200 Subject: [PATCH 065/141] refactor(search): restore inline attachment Signed-off-by: Camila Belo --- plugins/search-react/src/alpha.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index f28bc44b0d..322bd3e67b 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -89,11 +89,6 @@ export function createSearchResultListItemExtension< >(options: SearchResultItemExtensionOptions) { const id = `plugin.search.result.item.${options.id}`; - const attachTo = options.attachTo ?? { - id: 'plugin.search.page', - input: 'items', - }; - const configSchema = 'configSchema' in options ? options.configSchema @@ -105,7 +100,10 @@ export function createSearchResultListItemExtension< return createExtension({ id, - attachTo, + attachTo: options.attachTo ?? { + id: 'plugin.search.page', + input: 'items', + }, configSchema, output: { item: searchResultItemExtensionData, From 66af240ea692929df76d8f817e96595d648c44d6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 19 Oct 2023 14:54:35 +0200 Subject: [PATCH 066/141] refactor(frontend-plugin-api): merge boundary and suspense components Signed-off-by: Camila Belo --- packages/frontend-plugin-api/api-report.md | 13 +---- .../src/components/ErrorBoundary.tsx | 3 +- .../src/components/ExtensionBoundary.test.tsx | 6 +-- .../src/components/ExtensionBoundary.tsx | 47 ++++++++++++++----- .../src/components/index.ts | 5 -- .../src/extensions/createPageExtension.tsx | 41 ++++------------ plugins/catalog/src/alpha.tsx | 5 +- plugins/search-react/src/alpha.tsx | 17 +++---- 8 files changed, 57 insertions(+), 80 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index b6f85e757b..ac8d164fae 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -340,7 +340,7 @@ export interface ExtensionBoundaryProps { // (undocumented) id: string; // (undocumented) - routeRef?: RouteRef; + routable?: boolean; // (undocumented) source?: BackstagePlugin; } @@ -416,17 +416,6 @@ export interface ExtensionOverridesOptions { extensions: Extension[]; } -// @public (undocumented) -export function ExtensionSuspense( - props: ExtensionSuspenseProps, -): React_2.JSX.Element; - -// @public (undocumented) -export interface ExtensionSuspenseProps { - // (undocumented) - children: ReactNode; -} - // @public export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, diff --git a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx index f9ce9c9809..1191b75a1d 100644 --- a/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ErrorBoundary.tsx @@ -15,9 +15,10 @@ */ import React, { Component, PropsWithChildren } from 'react'; +// TODO: Dependency on MUI should be removed from core packages import { Button } from '@material-ui/core'; -import { BackstagePlugin } from '../wiring'; import { ErrorPanel } from '@backstage/core-components'; +import { BackstagePlugin } from '../wiring'; type DefaultErrorBoundaryFallbackProps = PropsWithChildren<{ plugin?: BackstagePlugin; diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 150ace717c..704cafda2d 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -33,7 +33,6 @@ import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; import { createApp } from '@backstage/frontend-app-api'; import { JsonObject } from '@backstage/types'; import { createRouteRef } from '../routing'; -import { toInternalRouteRef } from '../routing/RouteRef'; function renderExtensionInTestApp( extension: Extension, @@ -59,7 +58,6 @@ function renderExtensionInTestApp( const wrapInBoundaryExtension = (element: JSX.Element) => { const id = 'plugin.extension'; const routeRef = createRouteRef(); - toInternalRouteRef(routeRef).setId(id); return createExtension({ id, attachTo: { id: 'core.routes', input: 'routes' }, @@ -73,7 +71,7 @@ const wrapInBoundaryExtension = (element: JSX.Element) => { routeRef, path: '/', element: ( - + {element} ), @@ -128,7 +126,7 @@ describe('ExtensionBoundary', () => { subject, context: { extension: 'plugin.extension', - routeRef: 'plugin.extension', + routeRef: 'unknown', }, }), ); diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index 796908c619..a9ea297216 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -14,36 +14,59 @@ * limitations under the License. */ -import React, { ReactNode } from 'react'; -import { AnalyticsContext } from '@backstage/core-plugin-api'; +import React, { PropsWithChildren, ReactNode, useEffect } from 'react'; +import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '../wiring'; -import { RouteRef } from '../routing'; import { ErrorBoundary } from './ErrorBoundary'; -import { toInternalRouteRef } from '../routing/RouteRef'; +import { ExtensionSuspense } from './ExtensionSuspense'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; + +type RouteTrackerProps = PropsWithChildren<{ + disableTracking?: boolean; +}>; + +const RouteTracker = (props: RouteTrackerProps) => { + const { disableTracking, children } = props; + const analytics = useAnalytics(); + + // This event, never exposed to end-users of the analytics API, + // helps inform which extension metadata gets associated with a + // navigation event when the route navigated to is a gathered + // mountpoint. + useEffect(() => { + if (disableTracking) return; + analytics.captureEvent(routableExtensionRenderedEvent, ''); + }, [analytics, disableTracking]); + + return <>{children}; +}; /** @public */ export interface ExtensionBoundaryProps { id: string; source?: BackstagePlugin; - routeRef?: RouteRef; + routable?: boolean; children: ReactNode; } /** @public */ export function ExtensionBoundary(props: ExtensionBoundaryProps) { - const { id, source, routeRef, children } = props; + const { id, source, routable, children } = props; + // Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight const attributes = { extension: id, pluginId: source?.id, - routeRef: routeRef - ? toInternalRouteRef(routeRef).getDescription() - : undefined, }; return ( - - {children} - + + + + {children} + + + ); } diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index fecd7f36cc..7056a59271 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -18,8 +18,3 @@ export { ExtensionBoundary, type ExtensionBoundaryProps, } from './ExtensionBoundary'; - -export { - ExtensionSuspense, - type ExtensionSuspenseProps, -} from './ExtensionSuspense'; diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index ad7db1d222..f05182149b 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -14,11 +14,8 @@ * limitations under the License. */ -import React, { lazy, useEffect } from 'react'; -import { useAnalytics } from '@backstage/core-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; -import { ExtensionBoundary, ExtensionSuspense } from '../components'; +import React, { lazy } from 'react'; +import { ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../schema'; import { coreExtensionData, @@ -58,9 +55,7 @@ export function createPageExtension< }) => Promise; }, ): Extension { - const { id, routeRef } = options; - - const attachTo = options.attachTo ?? { id: 'core.routes', input: 'routes' }; + const { id } = options; const configSchema = 'configSchema' in options @@ -71,7 +66,7 @@ export function createPageExtension< return createExtension({ id, - attachTo, + attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' }, configSchema, inputs: options.inputs, disabled: options.disabled, @@ -81,36 +76,18 @@ export function createPageExtension< routeRef: coreExtensionData.routeRef.optional(), }, factory({ bind, config, inputs, source }) { - const { path } = config; - - const PageComponent = lazy(() => + const ExtensionComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), ); - const ExtensionComponent = () => { - const analytics = useAnalytics(); - - // This event, never exposed to end-users of the analytics API, - // helps inform which extension metadata gets associated with a - // navigation event when the route navigated to is a gathered - // mountpoint. - useEffect(() => { - analytics.captureEvent(routableExtensionRenderedEvent, ''); - }, [analytics]); - - return ; - }; - bind({ - path, - routeRef, + path: config.path, + routeRef: options.routeRef, element: ( - - - - + + ), }); diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index e9cb083978..31551c1930 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -36,7 +36,6 @@ import { PortableSchema, ExtensionBoundary, createExtensionInput, - ExtensionSuspense, } from '@backstage/frontend-plugin-api'; import { AsyncEntityProvider, @@ -117,9 +116,7 @@ export function createCatalogFilterExtension< bind({ element: ( - - - + ), }); diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index 322bd3e67b..f937ef99cc 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -20,7 +20,6 @@ import { ListItemProps } from '@material-ui/core'; import { ExtensionBoundary, - ExtensionSuspense, PortableSchema, createExtension, createExtensionDataRef, @@ -120,15 +119,13 @@ export function createSearchResultListItemExtension< predicate: options.predicate, component: props => ( - - - - - + + + ), }, From 052998a180c8ee0b3351e26dcea849ad17d458f7 Mon Sep 17 00:00:00 2001 From: Niklas Aronsson Date: Thu, 19 Oct 2023 14:55:07 +0200 Subject: [PATCH 067/141] Use setContent instead of clear plus addContent Signed-off-by: Niklas Aronsson --- .../src/actions/fetch/cookiecutter.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts index 3872b2e302..af63ca0e48 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.test.ts @@ -46,7 +46,7 @@ jest.mock( ); describe('fetch:cookiecutter', () => { - const mockDir = createMockDirectory(); + const mockDir = createMockDirectory({ mockOsTmpDir: true }); const integrations = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -106,21 +106,20 @@ describe('fetch:cookiecutter', () => { output: jest.fn(), createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; - mockDir.clear(); - mockDir.addContent({ template: {} }); + mockDir.setContent({ template: {} }); commandExists.mockResolvedValue(null); // Mock when run container is called it creates some new files in the mock filesystem containerRunner.runContainer.mockImplementation(async () => { - mockDir.addContent({ + mockDir.setContent({ 'intermediate/testfile.json': '{}', }); }); // Mock when executeShellCommand is called it creates some new files in the mock filesystem executeShellCommand.mockImplementation(async () => { - mockDir.addContent({ + mockDir.setContent({ 'intermediate/testfile.json': '{}', }); }); From 81c8db208884271920b16682535fea161d77a7e1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 16:34:02 +0200 Subject: [PATCH 068/141] core-components: make RoutedTabs not explode when rendering without tabs Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .changeset/dirty-ducks-behave.md | 5 +++++ .../src/components/TabbedLayout/RoutedTabs.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/dirty-ducks-behave.md diff --git a/.changeset/dirty-ducks-behave.md b/.changeset/dirty-ducks-behave.md new file mode 100644 index 0000000000..c7bea8ea0a --- /dev/null +++ b/.changeset/dirty-ducks-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fix `RoutedTabs` so that it does not explode without tabs. diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx index 2a0b0eae28..2c727efe1b 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx @@ -27,8 +27,8 @@ import { SubRoute } from './types'; export function useSelectedSubRoute(subRoutes: SubRoute[]): { index: number; - route: SubRoute; - element: JSX.Element; + route?: SubRoute; + element?: JSX.Element; } { const params = useParams(); @@ -44,7 +44,7 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): { b.path.replace(/\/\*$/, '').localeCompare(a.path.replace(/\/\*$/, '')), ); - const element = useRoutes(sortedRoutes) ?? subRoutes[0].children; + const element = useRoutes(sortedRoutes) ?? subRoutes[0]?.children; // TODO(Rugvip): Once we only support v6 stable we can always prefix // This avoids having a double / prefix for react-router v6 beta, which in turn breaks @@ -98,7 +98,7 @@ export function RoutedTabs(props: { routes: SubRoute[] }) { onChange={onTabChange} /> - + {element} From dc613f9bcfcdad42e2e5911d766ca5a7ae093cd0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 17:33:37 +0200 Subject: [PATCH 069/141] frontend-app-api: fix configuration schema Signed-off-by: Patrik Oldsberg --- .changeset/olive-paws-divide.md | 5 +++++ packages/frontend-app-api/config.d.ts | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/olive-paws-divide.md diff --git a/.changeset/olive-paws-divide.md b/.changeset/olive-paws-divide.md new file mode 100644 index 0000000000..688b6025bf --- /dev/null +++ b/.changeset/olive-paws-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Updated `app.extensions` configuration schema. diff --git a/packages/frontend-app-api/config.d.ts b/packages/frontend-app-api/config.d.ts index 9ec020fa56..28355f5376 100644 --- a/packages/frontend-app-api/config.d.ts +++ b/packages/frontend-app-api/config.d.ts @@ -34,17 +34,17 @@ export interface Config { /** * @deepVisibility frontend */ - extensions?: + extensions?: Array< | string | { [extensionId: string]: | boolean - | string | { - at?: string; - extension?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; config?: unknown; }; - }; + } + >; }; } From 6c6f392c74bec7ad04d9d1ec3b95a170d1a08ed4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Oct 2023 13:03:23 +0200 Subject: [PATCH 070/141] frontend-app-api: extract extension config reading into separate module Signed-off-by: Patrik Oldsberg --- .../frontend-app-api/src/wiring/createApp.tsx | 4 +- .../graph/readAppExtensionsConfig.test.ts | 268 ++++++++++++++++++ .../wiring/graph/readAppExtensionsConfig.ts | 199 +++++++++++++ .../src/wiring/parameters.test.ts | 254 +---------------- .../frontend-app-api/src/wiring/parameters.ts | 184 +----------- 5 files changed, 471 insertions(+), 438 deletions(-) create mode 100644 packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.test.ts create mode 100644 packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.ts diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index e770a6b4c1..dc132b6654 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -35,7 +35,6 @@ import { import { ExtensionInstanceParameters, mergeExtensionParameters, - readAppExtensionParameters, } from './parameters'; import { AnyApiFactory, @@ -96,6 +95,7 @@ import { AppRouteBinder } from '../routing'; import { RoutingProvider } from '../routing/RoutingProvider'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; +import { readAppExtensionsConfig } from './graph/readAppExtensionsConfig'; /** @public */ export interface ExtensionTreeNode { @@ -200,7 +200,7 @@ export function createInstances(options: { const extensionParams = mergeExtensionParameters({ features: options.features, builtinExtensions, - parameters: readAppExtensionParameters(options.config), + parameters: readAppExtensionsConfig(options.config), }); // TODO: validate the config of all extension instances diff --git a/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.test.ts b/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.test.ts new file mode 100644 index 0000000000..91f6992f29 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.test.ts @@ -0,0 +1,268 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; +import { + expandShorthandExtensionParameters, + readAppExtensionsConfig, +} from './readAppExtensionsConfig'; + +describe('readAppExtensionsConfig', () => { + it('should disable extension with shorthand notation', () => { + expect( + readAppExtensionsConfig( + new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: true, + }, + ]); + expect( + readAppExtensionsConfig( + new ConfigReader({ + app: { extensions: [{ 'core.router': { disabled: true } }] }, + }), + ), + ).toEqual([ + { + at: undefined, + config: undefined, + disabled: true, + id: 'core.router', + }, + ]); + }); + + it('should enable extension with shorthand notation', () => { + expect( + readAppExtensionsConfig( + new ConfigReader({ app: { extensions: ['core.router'] } }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: false, + }, + ]); + expect( + readAppExtensionsConfig( + new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: false, + }, + ]); + expect( + readAppExtensionsConfig( + new ConfigReader({ + app: { extensions: [{ 'core.router': { disabled: false } }] }, + }), + ), + ).toEqual([ + { + id: 'core.router', + disabled: false, + }, + ]); + }); + + it('should not allow string keys', () => { + expect(() => + readAppExtensionsConfig( + new ConfigReader({ + app: { + extensions: [{ 'core.router': 'some-string' }], + }, + }), + ), + ).toThrow( + 'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object', + ); + }); + + it('should not allow invalid keys', () => { + expect(() => + readAppExtensionsConfig( + new ConfigReader({ + app: { + extensions: [ + { + 'core.router/routes': { + extension: 'example-package#MyPage', + config: { foo: 'bar' }, + }, + }, + ], + }, + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[0], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`, + ); + }); +}); + +describe('expandShorthandExtensionParameters', () => { + const run = (value: JsonValue) => { + return expandShorthandExtensionParameters(value, 1); + }; + + it('rejects unknown keys', () => { + expect(() => run(null)).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, + ); + expect(() => run(1)).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, + ); + expect(() => run([])).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, + ); + }); + + it('rejects the wrong number of keys', () => { + expect(() => run({})).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must have exactly one key, got none"`, + ); + expect(() => run({ a: {}, b: {} })).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], must have exactly one key, got 'a', 'b'"`, + ); + }); + + it('rejects unknown values', () => { + expect(() => run({ a: 1 })).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`, + ); + expect(() => run({ a: [] })).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`, + ); + }); + + it('supports string key', () => { + expect(run('core.router')).toEqual({ + id: 'core.router', + disabled: false, + }); + expect(() => run('')).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`, + ); + expect(() => run(' a')).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`, + ); + expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`, + ); + }); + + it('supports null value', () => { + // this is the result of typing: + // - core.router: + // The missing value is interpreted as null by the yaml parser so we deal with that + expect(run({ 'core.router': null })).toEqual({ + id: 'core.router', + disabled: false, + }); + }); + + it('supports boolean value', () => { + expect(run({ 'core.router': true })).toEqual({ + id: 'core.router', + disabled: false, + }); + expect(run({ 'core.router': false })).toEqual({ + id: 'core.router', + disabled: true, + }); + }); + + it('should not support string values', () => { + expect(() => + run({ 'core.router': 'example-package#MyRouter' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router], value must be a boolean or object"`, + ); + }); + + it('supports object id only in the key', () => { + expect(() => + run({ 'core.router': { id: 'some.id' } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, + ); + }); + + it('supports object attachTo', () => { + expect( + run({ + 'core.router': { attachTo: { id: 'other.root', input: 'inputs' } }, + }), + ).toEqual({ + id: 'core.router', + attachTo: { id: 'other.root', input: 'inputs' }, + }); + expect(() => + run({ + 'core.router': { + id: 'other-id', + }, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, + ); + }); + + it('supports object disabled', () => { + expect(run({ 'core.router': { disabled: true } })).toEqual({ + id: 'core.router', + disabled: true, + }); + expect(run({ 'core.router': { disabled: false } })).toEqual({ + id: 'core.router', + disabled: false, + }); + expect(() => + run({ 'core.router': { disabled: 0 } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`, + ); + }); + + it('supports object config', () => { + expect( + run({ 'core.router': { config: { disableRedirects: true } } }), + ).toEqual({ + id: 'core.router', + config: { disableRedirects: true }, + }); + expect(() => + run({ 'core.router': { config: 0 } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`, + ); + }); + + it('rejects unknown object keys', () => { + expect(() => + run({ 'core.router': { foo: { settings: true } } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, + ); + }); +}); diff --git a/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.ts b/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.ts new file mode 100644 index 0000000000..2b5b51ce92 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.ts @@ -0,0 +1,199 @@ +/* + * Copyright 2023 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 { Config } from '@backstage/config'; +import { JsonValue } from '@backstage/types'; + +export interface ExtensionParameters { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + config?: unknown; +} + +const knownExtensionParameters = ['attachTo', 'disabled', 'config']; + +// Since we'll never merge arrays in config the config reader context +// isn't too much of a help. Fall back to manual config reading logic +// as the Config interface makes it quite hard for us otherwise. +/** @internal */ +export function readAppExtensionsConfig( + rootConfig: Config, +): ExtensionParameters[] { + const arr = rootConfig.getOptional('app.extensions'); + if (!Array.isArray(arr)) { + if (arr === undefined) { + return []; + } + // This will throw, and show which part of config had the wrong type + rootConfig.getConfigArray('app.extensions'); + return []; + } + + return arr.map((arrayEntry, arrayIndex) => + expandShorthandExtensionParameters(arrayEntry, arrayIndex), + ); +} + +/** @internal */ +export function expandShorthandExtensionParameters( + arrayEntry: JsonValue, + arrayIndex: number, +): ExtensionParameters { + function errorMsg(msg: string, key?: string, prop?: string) { + return `Invalid extension configuration at app.extensions[${arrayIndex}]${ + key ? `[${key}]` : '' + }${prop ? `.${prop}` : ''}, ${msg}`; + } + + // NOTE(freben): This check is intentionally not complete and doesn't check + // whether letters and digits are used, etc. It's not up to the config reading + // logic to decide what constitutes a valid extension ID; that should be + // decided by the logic that loads and instantiates the extensions. This check + // is just here to catch real mistakes or truly conceptually wrong input. + function assertValidId(id: string) { + if (!id || id !== id.trim()) { + throw new Error( + errorMsg('extension ID must not be empty or contain whitespace'), + ); + } + + if (id.includes('/')) { + let message = `extension ID must not contain slashes; got '${id}'`; + const good = id.split('/')[0]; + if (good) { + message += `, did you mean '${good}'?`; + } + throw new Error(errorMsg(message)); + } + } + + // Example YAML: + // - entity.card.about + if (typeof arrayEntry === 'string') { + assertValidId(arrayEntry); + return { + id: arrayEntry, + disabled: false, + }; + } + + // All remaining cases are single-key objects + if ( + typeof arrayEntry !== 'object' || + arrayEntry === null || + Array.isArray(arrayEntry) + ) { + throw new Error(errorMsg('must be a string or an object')); + } + const keys = Object.keys(arrayEntry); + if (keys.length !== 1) { + const joinedKeys = keys.length ? `'${keys.join("', '")}'` : 'none'; + throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`)); + } + + const id = String(keys[0]); + const value = arrayEntry[id]; + assertValidId(id); + + // This example covers a potentially common mistake in the syntax + // Example YAML: + // - entity.card.about: + if (value === null) { + return { + id, + disabled: false, + }; + } + + // Example YAML: + // - catalog.page.cicd: false + if (typeof value === 'boolean') { + return { + id, + disabled: !value, + }; + } + + // The remaining case is the generic object. Example YAML: + // - tech-radar.page: + // at: core.router/routes + // disabled: false + // config: + // path: /tech-radar + // width: 1500 + // height: 800 + if (typeof value !== 'object' || Array.isArray(value)) { + // We don't mention null here - we don't want people to explicitly enter + // - entity.card.about: null + throw new Error(errorMsg('value must be a boolean or object', id)); + } + + const attachTo = value.attachTo as { id: string; input: string } | undefined; + const disabled = value.disabled; + const config = value.config; + + if (attachTo !== undefined) { + if ( + attachTo === null || + typeof attachTo !== 'object' || + Array.isArray(attachTo) + ) { + throw new Error(errorMsg('must be an object', id, 'attachTo')); + } + if (typeof attachTo.id !== 'string' || attachTo.id === '') { + throw new Error( + errorMsg('must be a non-empty string', id, 'attachTo.id'), + ); + } + if (typeof attachTo.input !== 'string' || attachTo.input === '') { + throw new Error( + errorMsg('must be a non-empty string', id, 'attachTo.input'), + ); + } + } + if (disabled !== undefined && typeof disabled !== 'boolean') { + throw new Error(errorMsg('must be a boolean', id, 'disabled')); + } + if ( + config !== undefined && + (typeof config !== 'object' || config === null || Array.isArray(config)) + ) { + throw new Error(errorMsg('must be an object', id, 'config')); + } + + const unknownKeys = Object.keys(value).filter( + k => !knownExtensionParameters.includes(k), + ); + if (unknownKeys.length > 0) { + throw new Error( + errorMsg( + `unknown parameter; expected one of '${knownExtensionParameters.join( + "', '", + )}'`, + id, + unknownKeys.join(', '), + ), + ); + } + + return { + id, + attachTo, + disabled, + config, + }; +} diff --git a/packages/frontend-app-api/src/wiring/parameters.test.ts b/packages/frontend-app-api/src/wiring/parameters.test.ts index 4ee420f667..89d9c0c3f3 100644 --- a/packages/frontend-app-api/src/wiring/parameters.test.ts +++ b/packages/frontend-app-api/src/wiring/parameters.test.ts @@ -14,18 +14,12 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; import { createExtensionOverrides, createPlugin, Extension, } from '@backstage/frontend-plugin-api'; -import { JsonValue } from '@backstage/types'; -import { - expandShorthandExtensionParameters, - mergeExtensionParameters, - readAppExtensionParameters, -} from './parameters'; +import { mergeExtensionParameters } from './parameters'; function makeExt( id: string, @@ -208,249 +202,3 @@ describe('mergeExtensionParameters', () => { expect(result.map(r => r.extension.id)).toEqual(['b', 'c', 'a']); }); }); - -describe('readAppExtensionParameters', () => { - it('should disable extension with shorthand notation', () => { - expect( - readAppExtensionParameters( - new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }), - ), - ).toEqual([ - { - id: 'core.router', - disabled: true, - }, - ]); - expect( - readAppExtensionParameters( - new ConfigReader({ - app: { extensions: [{ 'core.router': { disabled: true } }] }, - }), - ), - ).toEqual([ - { - at: undefined, - config: undefined, - disabled: true, - id: 'core.router', - }, - ]); - }); - - it('should enable extension with shorthand notation', () => { - expect( - readAppExtensionParameters( - new ConfigReader({ app: { extensions: ['core.router'] } }), - ), - ).toEqual([ - { - id: 'core.router', - disabled: false, - }, - ]); - expect( - readAppExtensionParameters( - new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }), - ), - ).toEqual([ - { - id: 'core.router', - disabled: false, - }, - ]); - expect( - readAppExtensionParameters( - new ConfigReader({ - app: { extensions: [{ 'core.router': { disabled: false } }] }, - }), - ), - ).toEqual([ - { - id: 'core.router', - disabled: false, - }, - ]); - }); - - it('should not allow string keys', () => { - expect(() => - readAppExtensionParameters( - new ConfigReader({ - app: { - extensions: [{ 'core.router': 'some-string' }], - }, - }), - ), - ).toThrow( - 'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object', - ); - }); - - it('should not allow invalid keys', () => { - expect(() => - readAppExtensionParameters( - new ConfigReader({ - app: { - extensions: [ - { - 'core.router/routes': { - extension: 'example-package#MyPage', - config: { foo: 'bar' }, - }, - }, - ], - }, - }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[0], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`, - ); - }); -}); - -describe('expandShorthandExtensionParameters', () => { - const run = (value: JsonValue) => { - return expandShorthandExtensionParameters(value, 1); - }; - - it('rejects unknown keys', () => { - expect(() => run(null)).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, - ); - expect(() => run(1)).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, - ); - expect(() => run([])).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], must be a string or an object"`, - ); - }); - - it('rejects the wrong number of keys', () => { - expect(() => run({})).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], must have exactly one key, got none"`, - ); - expect(() => run({ a: {}, b: {} })).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], must have exactly one key, got 'a', 'b'"`, - ); - }); - - it('rejects unknown values', () => { - expect(() => run({ a: 1 })).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`, - ); - expect(() => run({ a: [] })).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`, - ); - }); - - it('supports string key', () => { - expect(run('core.router')).toEqual({ - id: 'core.router', - disabled: false, - }); - expect(() => run('')).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`, - ); - expect(() => run(' a')).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`, - ); - expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`, - ); - }); - - it('supports null value', () => { - // this is the result of typing: - // - core.router: - // The missing value is interpreted as null by the yaml parser so we deal with that - expect(run({ 'core.router': null })).toEqual({ - id: 'core.router', - disabled: false, - }); - }); - - it('supports boolean value', () => { - expect(run({ 'core.router': true })).toEqual({ - id: 'core.router', - disabled: false, - }); - expect(run({ 'core.router': false })).toEqual({ - id: 'core.router', - disabled: true, - }); - }); - - it('should not support string values', () => { - expect(() => - run({ 'core.router': 'example-package#MyRouter' }), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router], value must be a boolean or object"`, - ); - }); - - it('supports object id only in the key', () => { - expect(() => - run({ 'core.router': { id: 'some.id' } }), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, - ); - }); - - it('supports object attachTo', () => { - expect( - run({ - 'core.router': { attachTo: { id: 'other.root', input: 'inputs' } }, - }), - ).toEqual({ - id: 'core.router', - attachTo: { id: 'other.root', input: 'inputs' }, - }); - expect(() => - run({ - 'core.router': { - id: 'other-id', - }, - }), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, - ); - }); - - it('supports object disabled', () => { - expect(run({ 'core.router': { disabled: true } })).toEqual({ - id: 'core.router', - disabled: true, - }); - expect(run({ 'core.router': { disabled: false } })).toEqual({ - id: 'core.router', - disabled: false, - }); - expect(() => - run({ 'core.router': { disabled: 0 } }), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`, - ); - }); - - it('supports object config', () => { - expect( - run({ 'core.router': { config: { disableRedirects: true } } }), - ).toEqual({ - id: 'core.router', - config: { disableRedirects: true }, - }); - expect(() => - run({ 'core.router': { config: 0 } }), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`, - ); - }); - - it('rejects unknown object keys', () => { - expect(() => - run({ 'core.router': { foo: { settings: true } } }), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, - ); - }); -}); diff --git a/packages/frontend-app-api/src/wiring/parameters.ts b/packages/frontend-app-api/src/wiring/parameters.ts index 911f12f416..329c9ea42a 100644 --- a/packages/frontend-app-api/src/wiring/parameters.ts +++ b/packages/frontend-app-api/src/wiring/parameters.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; import { BackstagePlugin, Extension, @@ -22,188 +21,7 @@ import { } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; -import { JsonValue } from '@backstage/types'; - -export interface ExtensionParameters { - id: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - config?: unknown; -} - -const knownExtensionParameters = ['attachTo', 'disabled', 'config']; - -// Since we'll never merge arrays in config the config reader context -// isn't too much of a help. Fall back to manual config reading logic -// as the Config interface makes it quite hard for us otherwise. -/** @internal */ -export function readAppExtensionParameters( - rootConfig: Config, -): ExtensionParameters[] { - const arr = rootConfig.getOptional('app.extensions'); - if (!Array.isArray(arr)) { - if (arr === undefined) { - return []; - } - // This will throw, and show which part of config had the wrong type - rootConfig.getConfigArray('app.extensions'); - return []; - } - - return arr.map((arrayEntry, arrayIndex) => - expandShorthandExtensionParameters(arrayEntry, arrayIndex), - ); -} - -/** @internal */ -export function expandShorthandExtensionParameters( - arrayEntry: JsonValue, - arrayIndex: number, -): ExtensionParameters { - function errorMsg(msg: string, key?: string, prop?: string) { - return `Invalid extension configuration at app.extensions[${arrayIndex}]${ - key ? `[${key}]` : '' - }${prop ? `.${prop}` : ''}, ${msg}`; - } - - // NOTE(freben): This check is intentionally not complete and doesn't check - // whether letters and digits are used, etc. It's not up to the config reading - // logic to decide what constitutes a valid extension ID; that should be - // decided by the logic that loads and instantiates the extensions. This check - // is just here to catch real mistakes or truly conceptually wrong input. - function assertValidId(id: string) { - if (!id || id !== id.trim()) { - throw new Error( - errorMsg('extension ID must not be empty or contain whitespace'), - ); - } - - if (id.includes('/')) { - let message = `extension ID must not contain slashes; got '${id}'`; - const good = id.split('/')[0]; - if (good) { - message += `, did you mean '${good}'?`; - } - throw new Error(errorMsg(message)); - } - } - - // Example YAML: - // - entity.card.about - if (typeof arrayEntry === 'string') { - assertValidId(arrayEntry); - return { - id: arrayEntry, - disabled: false, - }; - } - - // All remaining cases are single-key objects - if ( - typeof arrayEntry !== 'object' || - arrayEntry === null || - Array.isArray(arrayEntry) - ) { - throw new Error(errorMsg('must be a string or an object')); - } - const keys = Object.keys(arrayEntry); - if (keys.length !== 1) { - const joinedKeys = keys.length ? `'${keys.join("', '")}'` : 'none'; - throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`)); - } - - const id = String(keys[0]); - const value = arrayEntry[id]; - assertValidId(id); - - // This example covers a potentially common mistake in the syntax - // Example YAML: - // - entity.card.about: - if (value === null) { - return { - id, - disabled: false, - }; - } - - // Example YAML: - // - catalog.page.cicd: false - if (typeof value === 'boolean') { - return { - id, - disabled: !value, - }; - } - - // The remaining case is the generic object. Example YAML: - // - tech-radar.page: - // at: core.router/routes - // disabled: false - // config: - // path: /tech-radar - // width: 1500 - // height: 800 - if (typeof value !== 'object' || Array.isArray(value)) { - // We don't mention null here - we don't want people to explicitly enter - // - entity.card.about: null - throw new Error(errorMsg('value must be a boolean or object', id)); - } - - const attachTo = value.attachTo as { id: string; input: string } | undefined; - const disabled = value.disabled; - const config = value.config; - - if (attachTo !== undefined) { - if ( - attachTo === null || - typeof attachTo !== 'object' || - Array.isArray(attachTo) - ) { - throw new Error(errorMsg('must be an object', id, 'attachTo')); - } - if (typeof attachTo.id !== 'string' || attachTo.id === '') { - throw new Error( - errorMsg('must be a non-empty string', id, 'attachTo.id'), - ); - } - if (typeof attachTo.input !== 'string' || attachTo.input === '') { - throw new Error( - errorMsg('must be a non-empty string', id, 'attachTo.input'), - ); - } - } - if (disabled !== undefined && typeof disabled !== 'boolean') { - throw new Error(errorMsg('must be a boolean', id, 'disabled')); - } - if ( - config !== undefined && - (typeof config !== 'object' || config === null || Array.isArray(config)) - ) { - throw new Error(errorMsg('must be an object', id, 'config')); - } - - const unknownKeys = Object.keys(value).filter( - k => !knownExtensionParameters.includes(k), - ); - if (unknownKeys.length > 0) { - throw new Error( - errorMsg( - `unknown parameter; expected one of '${knownExtensionParameters.join( - "', '", - )}'`, - id, - unknownKeys.join(', '), - ), - ); - } - - return { - id, - attachTo, - disabled, - config, - }; -} +import { ExtensionParameters } from './graph/readAppExtensionsConfig'; export interface ExtensionInstanceParameters { extension: Extension; From c617af1d6d0cd34d225957b547bfef2ede802e7e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Oct 2023 13:04:16 +0200 Subject: [PATCH 071/141] frontend-app-api: add initial app graph types Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/index.ts | 22 +++++++ .../src/wiring/graph/types.ts | 65 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 packages/frontend-app-api/src/wiring/graph/index.ts create mode 100644 packages/frontend-app-api/src/wiring/graph/types.ts diff --git a/packages/frontend-app-api/src/wiring/graph/index.ts b/packages/frontend-app-api/src/wiring/graph/index.ts new file mode 100644 index 0000000000..608ecb7675 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 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 { + AppNode, + AppNodeEdges, + AppNodeInstance, + AppNodeSpec, +} from './types'; diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts new file mode 100644 index 0000000000..3c5cd5df1b --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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 { + BackstagePlugin, + Extension, + ExtensionDataRef, +} from '@backstage/frontend-plugin-api'; + +/** + * The specification for this node in the app graph. + * @public + */ +export interface AppNodeSpec { + id: string; + attachTo: { id: string; input: string }; + extension: Extension; + disabled: boolean; + config?: unknown; + source?: BackstagePlugin; +} + +/** + * The connections from this node to other nodes. + * @public + */ +export interface AppNodeEdges { + attachedTo: { node: AppNode; input: string }; + attachments: Map; +} + +/** + * The instance of this node in the app graph. + * @public + */ +export interface AppNodeInstance { + getDataRefs(): ExtensionDataRef[]; + getData(ref: ExtensionDataRef): T | unknown; +} + +/** + * + * @public + */ +export interface AppNode { + /** The specification for how this node should be instantiated */ + spec: AppNodeSpec; + /** The edges from this node to other nodes in the app graph */ + edges: AppNodeEdges; + /** The instance of this node, if it was instantiated */ + instance?: AppNodeInstance; +} From b3977b9864b55c284fa0cec0b9e622d6381923d0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Oct 2023 13:11:11 +0200 Subject: [PATCH 072/141] frontend-app-api: refactor mergeExtensionParameters -> resolveAppNodeSpecs Signed-off-by: Patrik Oldsberg --- .../frontend-app-api/src/wiring/createApp.tsx | 21 +++++++------------ .../resolveAppNodeSpecs.test.ts} | 18 ++++++++-------- .../resolveAppNodeSpecs.ts} | 20 +++++++----------- 3 files changed, 24 insertions(+), 35 deletions(-) rename packages/frontend-app-api/src/wiring/{parameters.test.ts => graph/resolveAppNodeSpecs.test.ts} (93%) rename packages/frontend-app-api/src/wiring/{parameters.ts => graph/resolveAppNodeSpecs.ts} (93%) diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index dc132b6654..b2c82dc8cf 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -32,10 +32,7 @@ import { createExtensionInstance, ExtensionInstance, } from './createExtensionInstance'; -import { - ExtensionInstanceParameters, - mergeExtensionParameters, -} from './parameters'; +import { resolveAppNodeSpecs } from './graph/resolveAppNodeSpecs'; import { AnyApiFactory, ApiHolder, @@ -96,6 +93,7 @@ import { RoutingProvider } from '../routing/RoutingProvider'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; import { readAppExtensionsConfig } from './graph/readAppExtensionsConfig'; +import { AppNodeSpec } from './graph'; /** @public */ export interface ExtensionTreeNode { @@ -197,7 +195,7 @@ export function createInstances(options: { // pull in default extension instance from discovered packages // apply config to adjust default extension instances and add more - const extensionParams = mergeExtensionParameters({ + const appNodeSpecs = resolveAppNodeSpecs({ features: options.features, builtinExtensions, parameters: readAppExtensionsConfig(options.config), @@ -207,11 +205,8 @@ export function createInstances(options: { // We do it at this point to ensure that merging (if any) of config has already happened // Create attachment map so that we can look attachments up during instance creation - const attachmentMap = new Map< - string, - Map - >(); - for (const instanceParams of extensionParams) { + const attachmentMap = new Map>(); + for (const instanceParams of appNodeSpecs) { const extensionId = instanceParams.attachTo.id; const pointId = instanceParams.attachTo.input; let pointMap = attachmentMap.get(extensionId); @@ -231,9 +226,7 @@ export function createInstances(options: { const instances = new Map(); - function createInstance( - instanceParams: ExtensionInstanceParameters, - ): ExtensionInstance { + function createInstance(instanceParams: AppNodeSpec): ExtensionInstance { const extensionId = instanceParams.extension.id; const existingInstance = instances.get(extensionId); if (existingInstance) { @@ -261,7 +254,7 @@ export function createInstances(options: { } const coreInstance = createInstance( - extensionParams.find(p => p.extension.id === 'core')!, + appNodeSpecs.find(p => p.extension.id === 'core')!, ); return { coreInstance, instances }; diff --git a/packages/frontend-app-api/src/wiring/parameters.test.ts b/packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.test.ts similarity index 93% rename from packages/frontend-app-api/src/wiring/parameters.test.ts rename to packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.test.ts index 89d9c0c3f3..1d3c0a26ed 100644 --- a/packages/frontend-app-api/src/wiring/parameters.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.test.ts @@ -19,7 +19,7 @@ import { createPlugin, Extension, } from '@backstage/frontend-plugin-api'; -import { mergeExtensionParameters } from './parameters'; +import { resolveAppNodeSpecs } from './resolveAppNodeSpecs'; function makeExt( id: string, @@ -33,10 +33,10 @@ function makeExt( } as Extension; } -describe('mergeExtensionParameters', () => { +describe('resolveAppNodeSpecs', () => { it('should filter out disabled extension instances', () => { expect( - mergeExtensionParameters({ + resolveAppNodeSpecs({ features: [], builtinExtensions: [makeExt('a', 'disabled')], parameters: [], @@ -48,7 +48,7 @@ describe('mergeExtensionParameters', () => { const a = makeExt('a'); const b = makeExt('b'); expect( - mergeExtensionParameters({ + resolveAppNodeSpecs({ features: [], builtinExtensions: [a, b], parameters: [], @@ -64,7 +64,7 @@ describe('mergeExtensionParameters', () => { const b = makeExt('b'); const pluginA = createPlugin({ id: 'test', extensions: [a] }); expect( - mergeExtensionParameters({ + resolveAppNodeSpecs({ features: [pluginA], builtinExtensions: [b], parameters: [ @@ -89,7 +89,7 @@ describe('mergeExtensionParameters', () => { const b = makeExt('b'); const plugin = createPlugin({ id: 'test', extensions: [a, b] }); expect( - mergeExtensionParameters({ + resolveAppNodeSpecs({ features: [plugin], builtinExtensions: [], parameters: [ @@ -127,7 +127,7 @@ describe('mergeExtensionParameters', () => { const a = makeExt('a', 'disabled'); const b = makeExt('b', 'disabled'); expect( - mergeExtensionParameters({ + resolveAppNodeSpecs({ features: [createPlugin({ id: 'empty', extensions: [] })], builtinExtensions: [a, b], parameters: [ @@ -155,7 +155,7 @@ describe('mergeExtensionParameters', () => { const bOverride = makeExt('b', 'disabled', 'other'); const cOverride = makeExt('c'); - const result = mergeExtensionParameters({ + const result = resolveAppNodeSpecs({ features: [ plugin, createExtensionOverrides({ @@ -188,7 +188,7 @@ describe('mergeExtensionParameters', () => { const bOverride = makeExt('b', 'disabled'); const cOverride = makeExt('a', 'disabled'); - const result = mergeExtensionParameters({ + const result = resolveAppNodeSpecs({ features: [ createPlugin({ id: 'test', extensions: [a, b, c] }), createExtensionOverrides({ diff --git a/packages/frontend-app-api/src/wiring/parameters.ts b/packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.ts similarity index 93% rename from packages/frontend-app-api/src/wiring/parameters.ts rename to packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.ts index 329c9ea42a..8cad54f4be 100644 --- a/packages/frontend-app-api/src/wiring/parameters.ts +++ b/packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.ts @@ -20,22 +20,16 @@ import { ExtensionOverrides, } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; -import { ExtensionParameters } from './graph/readAppExtensionsConfig'; - -export interface ExtensionInstanceParameters { - extension: Extension; - source?: BackstagePlugin; - attachTo: { id: string; input: string }; - config?: unknown; -} +import { toInternalExtensionOverrides } from '../../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; +import { ExtensionParameters } from './readAppExtensionsConfig'; +import { AppNodeSpec } from './types'; /** @internal */ -export function mergeExtensionParameters(options: { +export function resolveAppNodeSpecs(options: { features: (BackstagePlugin | ExtensionOverrides)[]; builtinExtensions: Extension[]; parameters: Array; -}): ExtensionInstanceParameters[] { +}): AppNodeSpec[] { const { builtinExtensions, parameters } = options; const plugins = options.features.filter( @@ -207,8 +201,10 @@ export function mergeExtensionParameters(options: { return configuredExtensions .filter(override => !override.params.disabled) .map(param => ({ - extension: param.extension, + id: param.extension.id, attachTo: param.params.attachTo, + extension: param.extension, + disabled: param.params.disabled, source: param.params.source, config: param.params.config, })); From d2bee21c914ddbaa7a2ef613411ec3b8fc18d32d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Oct 2023 00:30:46 +0200 Subject: [PATCH 073/141] frontend-app-api: add initial app graph builder Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/buildAppGraph.test.ts | 74 +++++++++++++ .../src/wiring/graph/buildAppGraph.ts | 104 ++++++++++++++++++ .../src/wiring/graph/types.ts | 26 +++-- 3 files changed, 193 insertions(+), 11 deletions(-) create mode 100644 packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts create mode 100644 packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts new file mode 100644 index 0000000000..1d5d8f415f --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2023 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 { createExtension } from '@backstage/frontend-plugin-api'; +import { buildAppGraph } from './buildAppGraph'; + +const extBaseConfig = { + id: 'test', + attachTo: { id: 'root', input: 'default' }, + output: {}, + factory() {}, +}; + +const extension = createExtension(extBaseConfig); + +const baseSpec = { extension, disabled: false }; + +describe('buildAppGraph', () => { + it('creates an empty graph', () => { + const graph = buildAppGraph([]); + expect([...graph.rootNodes.keys()]).toEqual([]); + expect([...graph.orphanNodes.keys()]).toEqual([]); + }); + + it('should create a graph', () => { + const graph = buildAppGraph([ + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ]); + expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); + expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); + }); + + it('should create a graph out of order', () => { + const graph = buildAppGraph([ + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, id: 'a' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ]); + expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); + expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); + }); + + it('throws an error when duplicated extensions are detected', () => { + expect(() => + buildAppGraph([ + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'a' }, + ]), + ).toThrow("Unexpected duplicate extension id 'a'"); + }); +}); diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts new file mode 100644 index 0000000000..151faa0e1d --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2023 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 { AppNode, AppNodeEdges, AppNodeSpec } from './types'; + +type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +/** + * Build the app graph by iterating through all node specs and constructing the app + * tree with all attachments in the same order as they appear in the input specs array. + * @internal + */ +export function buildAppGraph(specs: AppNodeSpec[]) { + const nodes = new Map(); + const rootNodes = new Map(); + // While iterating through the inputs specs we keep track of all nodes that were created + // before their parent, and attach them later when the parent is created. + // As we find the parents and attach the children, we remove them from this map. This means + // that after iterating through all input specs, this will be a map for each root node. + const orphansByParent = new Map< + string /* parentId */, + { orphan: AppNode; input: string }[] + >(); + + for (const spec of specs) { + // The main check with a more helpful error message happens in resolveAppNodeSpecs + if (nodes.has(spec.id)) { + throw new Error(`Unexpected duplicate extension id '${spec.id}'`); + } + + const node: AppNode = { + spec, + edges: { + attachments: new Map(), + }, + }; + nodes.set(spec.id, node); + + if (spec.attachTo) { + const parent = nodes.get(spec.attachTo.id); + if (parent) { + (node.edges as Mutable).attachedTo = { + node: parent, + input: spec.attachTo.input, + }; + const parentInputEdges = parent.edges.attachments.get( + spec.attachTo.input, + ); + if (parentInputEdges) { + parentInputEdges.push(node); + } else { + parent.edges.attachments.set(spec.attachTo.input, [node]); + } + } else { + const orphanNodesForParent = orphansByParent.get(spec.attachTo.id); + const orphan = { orphan: node, input: spec.attachTo.input }; + if (orphanNodesForParent) { + orphanNodesForParent.push(orphan); + } else { + orphansByParent.set(spec.attachTo.id, [orphan]); + } + } + } else { + rootNodes.set(spec.id, node); + } + + const orphanedChildren = orphansByParent.get(spec.id); + if (orphanedChildren) { + orphansByParent.delete(spec.id); + for (const { orphan, input } of orphanedChildren) { + (orphan.edges as Mutable).attachedTo = { node, input }; + const attachments = node.edges.attachments.get(input); + if (attachments) { + attachments.push(orphan); + } else { + node.edges.attachments.set(input, [orphan]); + } + } + } + } + + const orphanNodes = new Map( + Array.from(orphansByParent).flatMap(([, orphans]) => + orphans.map(({ orphan }) => [orphan.spec.id, orphan]), + ), + ); + + return { rootNodes, orphanNodes }; +} diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index 3c5cd5df1b..f6de7018f5 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -25,12 +25,12 @@ import { * @public */ export interface AppNodeSpec { - id: string; - attachTo: { id: string; input: string }; - extension: Extension; - disabled: boolean; - config?: unknown; - source?: BackstagePlugin; + readonly id: string; + readonly attachTo?: { id: string; input: string }; + readonly extension: Extension; + readonly disabled: boolean; + readonly config?: unknown; + readonly source?: BackstagePlugin; } /** @@ -38,8 +38,8 @@ export interface AppNodeSpec { * @public */ export interface AppNodeEdges { - attachedTo: { node: AppNode; input: string }; - attachments: Map; + readonly attachedTo?: { node: AppNode; input: string }; + readonly attachments: Map; } /** @@ -57,9 +57,13 @@ export interface AppNodeInstance { */ export interface AppNode { /** The specification for how this node should be instantiated */ - spec: AppNodeSpec; + readonly spec: AppNodeSpec; /** The edges from this node to other nodes in the app graph */ - edges: AppNodeEdges; + readonly edges: AppNodeEdges; /** The instance of this node, if it was instantiated */ - instance?: AppNodeInstance; + readonly instance?: AppNodeInstance; +} + +export interface AppGraph { + rootNodes: Map; } From 650309205b899fab980d7c20e89657191cc6328c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Oct 2023 17:44:29 +0200 Subject: [PATCH 074/141] frontend-app-api: move createExtensionInstance to graph Signed-off-by: Patrik Oldsberg --- .../src/routing/extractRouteInfoFromInstanceTree.ts | 2 +- packages/frontend-app-api/src/wiring/createApp.tsx | 2 +- .../src/wiring/{ => graph}/createExtensionInstance.test.ts | 0 .../src/wiring/{ => graph}/createExtensionInstance.ts | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename packages/frontend-app-api/src/wiring/{ => graph}/createExtensionInstance.test.ts (100%) rename packages/frontend-app-api/src/wiring/{ => graph}/createExtensionInstance.ts (100%) diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts index 0bf9c2d1dd..fadcb719b9 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts @@ -15,7 +15,7 @@ */ import { RouteRef, coreExtensionData } from '@backstage/frontend-plugin-api'; -import { ExtensionInstance } from '../wiring/createExtensionInstance'; +import { ExtensionInstance } from '../wiring/graph/createExtensionInstance'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toLegacyPlugin } from '../wiring/createApp'; import { BackstageRouteObject } from './types'; diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index b2c82dc8cf..485882c8eb 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -31,7 +31,7 @@ import { CoreNav } from '../extensions/CoreNav'; import { createExtensionInstance, ExtensionInstance, -} from './createExtensionInstance'; +} from './graph/createExtensionInstance'; import { resolveAppNodeSpecs } from './graph/resolveAppNodeSpecs'; import { AnyApiFactory, diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts b/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.test.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts rename to packages/frontend-app-api/src/wiring/graph/createExtensionInstance.test.ts diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/createExtensionInstance.ts rename to packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts From 7df6a42e4ad95bacaed4403ee7cab2aac273afb8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Oct 2023 17:44:49 +0200 Subject: [PATCH 075/141] frontend-app-api: extract Mutable to common internal type in graph Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts | 6 +----- packages/frontend-app-api/src/wiring/graph/types.ts | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts index 151faa0e1d..9cfb430bcb 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { AppNode, AppNodeEdges, AppNodeSpec } from './types'; - -type Mutable = { - -readonly [P in keyof T]: T[P]; -}; +import { AppNode, AppNodeEdges, AppNodeSpec, Mutable } from './types'; /** * Build the app graph by iterating through all node specs and constructing the app diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index f6de7018f5..88fc363d6f 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -20,6 +20,11 @@ import { ExtensionDataRef, } from '@backstage/frontend-plugin-api'; +/** @internal */ +export type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + /** * The specification for this node in the app graph. * @public From 544968da085cca68110f42af2675ce414ab9d762 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Oct 2023 18:01:45 +0200 Subject: [PATCH 076/141] frontend-app-api: make app node serializable Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/buildAppGraph.test.ts | 57 ++++++++++++++++ .../src/wiring/graph/buildAppGraph.ts | 68 ++++++++++++++++--- .../src/wiring/graph/types.ts | 3 +- 3 files changed, 119 insertions(+), 9 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts index 1d5d8f415f..21a5009a95 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts @@ -47,6 +47,49 @@ describe('buildAppGraph', () => { ]); expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); + + expect(JSON.parse(JSON.stringify([...graph.rootNodes.values()]))) + .toMatchInlineSnapshot(` + [ + { + "id": "a", + }, + { + "attachments": { + "x": [ + { + "id": "bx1", + }, + { + "id": "bx2", + }, + ], + "y": [ + { + "id": "by1", + }, + ], + }, + "id": "b", + }, + { + "id": "c", + }, + ] + `); + expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`""`); + expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(` + " + x [ + + + ] + y [ + + ] + " + `); + expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`""`); }); it('should create a graph out of order', () => { @@ -61,6 +104,20 @@ describe('buildAppGraph', () => { ]); expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); + + expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`""`); + expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(` + " + x [ + + + ] + y [ + + ] + " + `); + expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`""`); }); it('throws an error when duplicated extensions are detected', () => { diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts index 9cfb430bcb..79977f6993 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts @@ -14,14 +14,71 @@ * limitations under the License. */ -import { AppNode, AppNodeEdges, AppNodeSpec, Mutable } from './types'; +import { + AppGraph, + AppNode, + AppNodeEdges, + AppNodeInstance, + AppNodeSpec, + Mutable, +} from './types'; + +function indent(str: string) { + return str.replace(/^/gm, ' '); +} + +/** @internal */ +class SerializableAppNode implements AppNode { + public readonly spec: AppNodeSpec; + public readonly edges: AppNodeEdges = { attachments: new Map() }; + public readonly instance?: AppNodeInstance; + + constructor(spec: AppNodeSpec) { + this.spec = spec; + } + + toJSON() { + const dataRefs = this.instance && [...this.instance.getDataRefs()]; + return { + id: this.spec.id, + output: + dataRefs && dataRefs.length > 0 + ? dataRefs.map(ref => ref.id) + : undefined, + attachments: + this.edges.attachments.size > 0 + ? Object.fromEntries(this.edges.attachments) + : undefined, + }; + } + + toString() { + const dataRefs = this.instance && [...this.instance.getDataRefs()]; + const out = + dataRefs && dataRefs.length > 0 + ? ` out=[${[...dataRefs.keys()].join(', ')}]` + : ''; + + if (this.edges.attachments.size === 0) { + return `<${this.spec.id}${out} />`; + } + + return [ + `<${this.spec.id}${out}>`, + ...[...this.edges.attachments.entries()].map(([k, v]) => + indent([`${k} [`, ...v.map(e => indent(e.toString())), `]`].join('\n')), + ), + ``, + ].join('\n'); + } +} /** * Build the app graph by iterating through all node specs and constructing the app * tree with all attachments in the same order as they appear in the input specs array. * @internal */ -export function buildAppGraph(specs: AppNodeSpec[]) { +export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { const nodes = new Map(); const rootNodes = new Map(); // While iterating through the inputs specs we keep track of all nodes that were created @@ -39,12 +96,7 @@ export function buildAppGraph(specs: AppNodeSpec[]) { throw new Error(`Unexpected duplicate extension id '${spec.id}'`); } - const node: AppNode = { - spec, - edges: { - attachments: new Map(), - }, - }; + const node = new SerializableAppNode(spec); nodes.set(spec.id, node); if (spec.attachTo) { diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index 88fc363d6f..29c0028d66 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -52,7 +52,7 @@ export interface AppNodeEdges { * @public */ export interface AppNodeInstance { - getDataRefs(): ExtensionDataRef[]; + getDataRefs(): Iterable>; getData(ref: ExtensionDataRef): T | unknown; } @@ -71,4 +71,5 @@ export interface AppNode { export interface AppGraph { rootNodes: Map; + orphanNodes: Map; } From bb4d40b481d92f9e85157a20bc18064a7fb69adc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 01:04:50 +0200 Subject: [PATCH 077/141] frontend-app-api: refactor createExtensionInstance -> createAppNodeInstance Signed-off-by: Patrik Oldsberg --- ...onInstance.ts => createAppNodeInstance.ts} | 136 ++++++------------ 1 file changed, 41 insertions(+), 95 deletions(-) rename packages/frontend-app-api/src/wiring/graph/{createExtensionInstance.ts => createAppNodeInstance.ts} (58%) diff --git a/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts similarity index 58% rename from packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts rename to packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts index 767329da7f..310852cd1c 100644 --- a/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts @@ -18,10 +18,12 @@ import { AnyExtensionDataMap, AnyExtensionInputMap, BackstagePlugin, - Extension, ExtensionDataRef, } from '@backstage/frontend-plugin-api'; import mapValues from 'lodash/mapValues'; +import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; + +type AppNodeWithInstance = AppNode & { instance: AppNodeInstance }; /** @internal */ export interface ExtensionInstance { @@ -42,14 +44,14 @@ export interface ExtensionInstance { function resolveInputData( dataMap: AnyExtensionDataMap, - attachment: ExtensionInstance, + attachment: AppNodeWithInstance, inputName: string, ) { return mapValues(dataMap, ref => { - const value = attachment.getData(ref); + const value = attachment.instance.getData(ref); if (value === undefined && !ref.config.optional) { throw new Error( - `input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.id}'`, + `input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.spec.id}'`, ); } return value; @@ -58,7 +60,7 @@ function resolveInputData( function resolveInputs( inputMap: AnyExtensionInputMap, - attachments: Map, + attachments: Map, ) { const undeclaredAttachments = Array.from(attachments.entries()).filter( ([inputName]) => inputMap[inputName] === undefined, @@ -72,7 +74,7 @@ function resolveInputs( .map( ([k, exts]) => `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts - .map(e => e.id) + .map(e => e.spec.id) .join("', '")}'`, ) .join(' and ')}`, @@ -80,113 +82,54 @@ function resolveInputs( } return mapValues(inputMap, (input, inputName) => { - const attachedInstances = attachments.get(inputName) ?? []; + const attachedNodes = + attachments + .get(inputName) + ?.filter((node): node is AppNodeWithInstance => + Boolean(node.instance), + ) ?? []; + if (input.config.singleton) { - if (attachedInstances.length > 1) { + if (attachedNodes.length > 1) { + const attachedNodeIds = attachedNodes.map(e => e.spec.id); throw Error( `expected ${ input.config.optional ? 'at most' : 'exactly' - } one '${inputName}' input but received multiple: '${attachedInstances - .map(e => e.id) - .join("', '")}'`, + } one '${inputName}' input but received multiple: '${attachedNodeIds.join( + "', '", + )}'`, ); - } else if (attachedInstances.length === 0) { + } else if (attachedNodes.length === 0) { if (input.config.optional) { return undefined; } throw Error(`input '${inputName}' is required but was not received`); } - return resolveInputData( - input.extensionData, - attachedInstances[0], - inputName, - ); + return resolveInputData(input.extensionData, attachedNodes[0], inputName); } - return attachedInstances.map(attachment => + return attachedNodes.map(attachment => resolveInputData(input.extensionData, attachment, inputName), ); }); } -function indent(str: string) { - return str.replace(/^/gm, ' '); -} - -class ExtensionInstanceImpl implements ExtensionInstance { - readonly $$type = '@backstage/ExtensionInstance'; - - readonly id: string; - readonly #extensionData: Map; - readonly attachments: Map; - readonly source?: BackstagePlugin; - - constructor( - id: string, - extensionData: Map, - attachments: Map, - source: BackstagePlugin | undefined, - ) { - this.id = id; - this.#extensionData = extensionData; - this.attachments = attachments; - this.source = source; - } - - getData(ref: ExtensionDataRef): T | undefined { - return this.#extensionData.get(ref.id) as T | undefined; - } - - toJSON() { - return { - id: this.id, - output: - this.#extensionData.size > 0 - ? [...this.#extensionData.keys()] - : undefined, - attachments: - this.attachments.size > 0 - ? Object.fromEntries(this.attachments) - : undefined, - }; - } - - toString() { - const out = - this.#extensionData.size > 0 - ? ` out=[${[...this.#extensionData.keys()].join(', ')}]` - : ''; - - if (this.attachments.size === 0) { - return `<${this.id}${out} />`; - } - - return [ - `<${this.id}${out}>`, - ...[...this.attachments.entries()].map(([k, v]) => - indent([`${k} [`, ...v.map(e => indent(e.toString())), `]`].join('\n')), - ), - ``, - ].join('\n'); - } -} - /** @internal */ -export function createExtensionInstance(options: { - extension: Extension; - config: unknown; - source?: BackstagePlugin; - attachments: Map; -}): ExtensionInstance { - const { extension, config, source, attachments } = options; +export function createAppNodeInstance(options: { + spec: AppNodeSpec; + attachments: Map; +}): AppNodeInstance { + const { spec, attachments } = options; + const { id, extension, config, source } = spec; const extensionData = new Map(); + const extensionDataRefs = new Set>(); let parsedConfig: unknown; try { parsedConfig = extension.configSchema?.parse(config ?? {}); } catch (e) { throw new Error( - `Invalid configuration for extension '${extension.id}'; caused by ${e}`, + `Invalid configuration for extension '${id}'; caused by ${e}`, ); } @@ -206,22 +149,25 @@ export function createExtensionInstance(options: { ); } extensionData.set(ref.id, output); + extensionDataRefs.add(ref); } }, inputs: resolveInputs(extension.inputs, attachments), }); } catch (e) { throw new Error( - `Failed to instantiate extension '${extension.id}'${ + `Failed to instantiate extension '${id}'${ e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` }`, ); } - return new ExtensionInstanceImpl( - options.extension.id, - extensionData, - attachments, - source, - ); + return { + getDataRefs() { + return extensionDataRefs.values(); + }, + getData(ref: ExtensionDataRef): T | undefined { + return extensionData.get(ref.id) as T | undefined; + }, + }; } From 2ff857bdb4d83eba66493bec3c892a611a7b7edc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 01:22:54 +0200 Subject: [PATCH 078/141] frontend-app-api: add instantiateAppNodeTree Signed-off-by: Patrik Oldsberg --- .../wiring/graph/instantiateAppNodeTree.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts new file mode 100644 index 0000000000..b01ad020b9 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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 { createAppNodeInstance } from './createAppNodeInstance'; +import { AppNode, Mutable } from './types'; + +export function instantiateAppNodeTree(rootNode: AppNode): void { + function createInstance(node: AppNode): void { + if (node.instance) { + return; + } + if (node.spec.disabled) { + return; + } + + for (const children of node.edges.attachments.values()) { + for (const child of children) { + createInstance(child); + } + } + + (node as Mutable).instance = createAppNodeInstance({ + spec: node.spec, + attachments: node.edges.attachments, + }); + + return; + } + + createInstance(rootNode); +} From 6e6458f9c2b47b060e3739f89fda42aa7ae010f6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 10:55:55 +0200 Subject: [PATCH 079/141] frontend-app-api: tweak app graph to have an explicit root node Signed-off-by: Patrik Oldsberg --- .../frontend-app-api/src/extensions/Core.tsx | 2 +- .../src/wiring/graph/buildAppGraph.test.ts | 142 ++++++++++-------- .../src/wiring/graph/buildAppGraph.ts | 29 ++-- .../src/wiring/graph/types.ts | 6 +- 4 files changed, 104 insertions(+), 75 deletions(-) diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 6efe3984dc..f48aa2ea6b 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -22,7 +22,7 @@ import { export const Core = createExtension({ id: 'core', - attachTo: { id: 'root', input: 'default' }, + attachTo: { id: 'root', input: 'default' }, // ignored inputs: { apis: createExtensionInput({ api: coreExtensionData.apiFactory, diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts index 21a5009a95..dc489565f4 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts @@ -19,66 +19,70 @@ import { buildAppGraph } from './buildAppGraph'; const extBaseConfig = { id: 'test', - attachTo: { id: 'root', input: 'default' }, + attachTo: { id: 'nonexistent', input: 'nonexistent' }, output: {}, factory() {}, }; const extension = createExtension(extBaseConfig); -const baseSpec = { extension, disabled: false }; +const baseSpec = { + extension, + attachTo: { id: 'nonexistent', input: 'nonexistent' }, + disabled: false, +}; describe('buildAppGraph', () => { - it('creates an empty graph', () => { - const graph = buildAppGraph([]); - expect([...graph.rootNodes.keys()]).toEqual([]); - expect([...graph.orphanNodes.keys()]).toEqual([]); + it('should fail to create an empty graph', () => { + expect(() => buildAppGraph([])).toThrow( + "No root node with id 'core' found in app graph", + ); + }); + + it('should create a graph with only one node', () => { + const graph = buildAppGraph([{ ...baseSpec, id: 'core' }]); + expect(graph.root).toEqual({ + spec: { ...baseSpec, id: 'core' }, + edges: { attachments: new Map() }, + }); + expect(Array.from(graph.orphans)).toEqual([]); }); it('should create a graph', () => { - const graph = buildAppGraph([ - { ...baseSpec, id: 'a' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); - expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); - expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); - - expect(JSON.parse(JSON.stringify([...graph.rootNodes.values()]))) - .toMatchInlineSnapshot(` + const graph = buildAppGraph( [ - { - "id": "a", + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + 'b', + ); + + expect(JSON.parse(JSON.stringify(graph.root))).toMatchInlineSnapshot(` + { + "attachments": { + "x": [ + { + "id": "bx1", + }, + { + "id": "bx2", + }, + ], + "y": [ + { + "id": "by1", + }, + ], }, - { - "attachments": { - "x": [ - { - "id": "bx1", - }, - { - "id": "bx2", - }, - ], - "y": [ - { - "id": "by1", - }, - ], - }, - "id": "b", - }, - { - "id": "c", - }, - ] + "id": "b", + } `); - expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`""`); - expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(` + expect(String(graph.root)).toMatchInlineSnapshot(` " x [ @@ -89,24 +93,32 @@ describe('buildAppGraph', () => { ] " `); - expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`""`); + + const orphans = Array.from(graph.orphans).map(String); + expect(orphans).toMatchInlineSnapshot(` + [ + "", + "", + "", + ] + `); }); it('should create a graph out of order', () => { - const graph = buildAppGraph([ - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, id: 'a' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); - expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); - expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); + const graph = buildAppGraph( + [ + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, id: 'a' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + 'b', + ); - expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`""`); - expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(` + expect(String(graph.root)).toMatchInlineSnapshot(` " x [ @@ -117,7 +129,15 @@ describe('buildAppGraph', () => { ] " `); - expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`""`); + + const orphans = Array.from(graph.orphans).map(String); + expect(orphans).toMatchInlineSnapshot(` + [ + "", + "", + "", + ] + `); }); it('throws an error when duplicated extensions are detected', () => { diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts index 79977f6993..72d4d90d48 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts @@ -78,9 +78,15 @@ class SerializableAppNode implements AppNode { * tree with all attachments in the same order as they appear in the input specs array. * @internal */ -export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { +export function buildAppGraph( + specs: AppNodeSpec[], + rootNodeId = 'core', +): AppGraph { const nodes = new Map(); - const rootNodes = new Map(); + + // A node with the provided rootNodeId must be found in the graph, and it must not be attached to anything + let rootNode: AppNode | undefined = undefined; + // While iterating through the inputs specs we keep track of all nodes that were created // before their parent, and attach them later when the parent is created. // As we find the parents and attach the children, we remove them from this map. This means @@ -99,7 +105,10 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { const node = new SerializableAppNode(spec); nodes.set(spec.id, node); - if (spec.attachTo) { + // TODO: For now we simply ignore the attachTo spec of the root node, but it'd be cleaner if we could avoid defining it + if (spec.id === rootNodeId) { + rootNode = node; + } else { const parent = nodes.get(spec.attachTo.id); if (parent) { (node.edges as Mutable).attachedTo = { @@ -123,8 +132,6 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { orphansByParent.set(spec.attachTo.id, [orphan]); } } - } else { - rootNodes.set(spec.id, node); } const orphanedChildren = orphansByParent.get(spec.id); @@ -142,11 +149,13 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { } } - const orphanNodes = new Map( - Array.from(orphansByParent).flatMap(([, orphans]) => - orphans.map(({ orphan }) => [orphan.spec.id, orphan]), - ), + const orphanNodes = Array.from(orphansByParent).flatMap(([, orphans]) => + orphans.map(({ orphan }) => orphan), ); - return { rootNodes, orphanNodes }; + if (!rootNode) { + throw new Error(`No root node with id '${rootNodeId}' found in app graph`); + } + + return { root: rootNode, orphans: orphanNodes }; } diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index 29c0028d66..6cf4ade360 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -31,7 +31,7 @@ export type Mutable = { */ export interface AppNodeSpec { readonly id: string; - readonly attachTo?: { id: string; input: string }; + readonly attachTo: { id: string; input: string }; readonly extension: Extension; readonly disabled: boolean; readonly config?: unknown; @@ -70,6 +70,6 @@ export interface AppNode { } export interface AppGraph { - rootNodes: Map; - orphanNodes: Map; + root: AppNode; + orphans: Iterable; } From ff33a006a1356f59122534e31b13e113809200ca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 11:01:30 +0200 Subject: [PATCH 080/141] frontend-app-api: add node map to app graph Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/buildAppGraph.test.ts | 21 +++++++++++++++++++ .../src/wiring/graph/buildAppGraph.ts | 2 +- .../src/wiring/graph/types.ts | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts index dc489565f4..8cf8279e96 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts @@ -46,6 +46,7 @@ describe('buildAppGraph', () => { edges: { attachments: new Map() }, }); expect(Array.from(graph.orphans)).toEqual([]); + expect(Array.from(graph.nodes.keys())).toEqual(['core']); }); it('should create a graph', () => { @@ -62,6 +63,16 @@ describe('buildAppGraph', () => { 'b', ); + expect(Array.from(graph.nodes.keys())).toEqual([ + 'a', + 'b', + 'c', + 'bx1', + 'bx2', + 'by1', + 'dx1', + ]); + expect(JSON.parse(JSON.stringify(graph.root))).toMatchInlineSnapshot(` { "attachments": { @@ -118,6 +129,16 @@ describe('buildAppGraph', () => { 'b', ); + expect(Array.from(graph.nodes.keys())).toEqual([ + 'bx2', + 'a', + 'by1', + 'b', + 'bx1', + 'c', + 'dx1', + ]); + expect(String(graph.root)).toMatchInlineSnapshot(` " x [ diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts index 72d4d90d48..f87d191908 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts @@ -157,5 +157,5 @@ export function buildAppGraph( throw new Error(`No root node with id '${rootNodeId}' found in app graph`); } - return { root: rootNode, orphans: orphanNodes }; + return { root: rootNode, nodes, orphans: orphanNodes }; } diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index 6cf4ade360..21ea55c780 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -71,5 +71,6 @@ export interface AppNode { export interface AppGraph { root: AppNode; + nodes: Map; orphans: Iterable; } From 3230953cc971bfa6da8e19c0b6055c156c495352 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 11:02:24 +0200 Subject: [PATCH 081/141] frontend-app-api: switch to ReadonlyMap in app graph types Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/wiring/graph/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index 21ea55c780..d7f80faa1b 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -44,7 +44,7 @@ export interface AppNodeSpec { */ export interface AppNodeEdges { readonly attachedTo?: { node: AppNode; input: string }; - readonly attachments: Map; + readonly attachments: ReadonlyMap; } /** @@ -71,6 +71,6 @@ export interface AppNode { export interface AppGraph { root: AppNode; - nodes: Map; + nodes: ReadonlyMap; orphans: Iterable; } From 1a9acc4ff19b6d53ba6e7ec67d0b50776a4a3928 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 11:10:26 +0200 Subject: [PATCH 082/141] frontend-app-api: refactor buildAppGraph to avoid mutability casts Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/buildAppGraph.ts | 71 ++++++++----------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts index f87d191908..06347b1ece 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - AppGraph, - AppNode, - AppNodeEdges, - AppNodeInstance, - AppNodeSpec, - Mutable, -} from './types'; +import { AppGraph, AppNode, AppNodeInstance, AppNodeSpec } from './types'; function indent(str: string) { return str.replace(/^/gm, ' '); @@ -30,13 +23,29 @@ function indent(str: string) { /** @internal */ class SerializableAppNode implements AppNode { public readonly spec: AppNodeSpec; - public readonly edges: AppNodeEdges = { attachments: new Map() }; + public readonly edges = { + attachedTo: undefined as { node: AppNode; input: string } | undefined, + attachments: new Map(), + }; public readonly instance?: AppNodeInstance; constructor(spec: AppNodeSpec) { this.spec = spec; } + setParent(parent: SerializableAppNode) { + const input = this.spec.attachTo.input; + + this.edges.attachedTo = { node: parent, input }; + + const parentInputEdges = parent.edges.attachments.get(input); + if (parentInputEdges) { + parentInputEdges.push(this); + } else { + parent.edges.attachments.set(input, [this]); + } + } + toJSON() { const dataRefs = this.instance && [...this.instance.getDataRefs()]; return { @@ -52,7 +61,7 @@ class SerializableAppNode implements AppNode { }; } - toString() { + toString(): string { const dataRefs = this.instance && [...this.instance.getDataRefs()]; const out = dataRefs && dataRefs.length > 0 @@ -82,7 +91,7 @@ export function buildAppGraph( specs: AppNodeSpec[], rootNodeId = 'core', ): AppGraph { - const nodes = new Map(); + const nodes = new Map(); // A node with the provided rootNodeId must be found in the graph, and it must not be attached to anything let rootNode: AppNode | undefined = undefined; @@ -93,7 +102,7 @@ export function buildAppGraph( // that after iterating through all input specs, this will be a map for each root node. const orphansByParent = new Map< string /* parentId */, - { orphan: AppNode; input: string }[] + SerializableAppNode[] >(); for (const spec of specs) { @@ -111,25 +120,13 @@ export function buildAppGraph( } else { const parent = nodes.get(spec.attachTo.id); if (parent) { - (node.edges as Mutable).attachedTo = { - node: parent, - input: spec.attachTo.input, - }; - const parentInputEdges = parent.edges.attachments.get( - spec.attachTo.input, - ); - if (parentInputEdges) { - parentInputEdges.push(node); - } else { - parent.edges.attachments.set(spec.attachTo.input, [node]); - } + node.setParent(parent); } else { const orphanNodesForParent = orphansByParent.get(spec.attachTo.id); - const orphan = { orphan: node, input: spec.attachTo.input }; if (orphanNodesForParent) { - orphanNodesForParent.push(orphan); + orphanNodesForParent.push(node); } else { - orphansByParent.set(spec.attachTo.id, [orphan]); + orphansByParent.set(spec.attachTo.id, [node]); } } } @@ -137,25 +134,19 @@ export function buildAppGraph( const orphanedChildren = orphansByParent.get(spec.id); if (orphanedChildren) { orphansByParent.delete(spec.id); - for (const { orphan, input } of orphanedChildren) { - (orphan.edges as Mutable).attachedTo = { node, input }; - const attachments = node.edges.attachments.get(input); - if (attachments) { - attachments.push(orphan); - } else { - node.edges.attachments.set(input, [orphan]); - } + for (const orphan of orphanedChildren) { + orphan.setParent(node); } } } - const orphanNodes = Array.from(orphansByParent).flatMap(([, orphans]) => - orphans.map(({ orphan }) => orphan), - ); - if (!rootNode) { throw new Error(`No root node with id '${rootNodeId}' found in app graph`); } - return { root: rootNode, nodes, orphans: orphanNodes }; + return { + root: rootNode, + nodes, + orphans: Array.from(orphansByParent.values()).flat(), + }; } From be2afe8d48597463ae17a86d8d07f75ca76526d4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 11:12:01 +0200 Subject: [PATCH 083/141] frontend-app-api: update createAppNodeInstance for ReadonlyMap + remove ExtensionInstance type Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/createAppNodeInstance.ts | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts index 310852cd1c..22cb3d4df7 100644 --- a/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts +++ b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts @@ -17,7 +17,6 @@ import { AnyExtensionDataMap, AnyExtensionInputMap, - BackstagePlugin, ExtensionDataRef, } from '@backstage/frontend-plugin-api'; import mapValues from 'lodash/mapValues'; @@ -25,23 +24,6 @@ import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; type AppNodeWithInstance = AppNode & { instance: AppNodeInstance }; -/** @internal */ -export interface ExtensionInstance { - readonly $$type: '@backstage/ExtensionInstance'; - - readonly id: string; - /** - * Get concrete value for the given extension data reference. Returns undefined if no value is available. - */ - getData(ref: ExtensionDataRef): T | undefined; - /** - * Maps input names to the actual instances given to them. - */ - readonly attachments: Map; - - readonly source?: BackstagePlugin; -} - function resolveInputData( dataMap: AnyExtensionDataMap, attachment: AppNodeWithInstance, @@ -60,7 +42,7 @@ function resolveInputData( function resolveInputs( inputMap: AnyExtensionInputMap, - attachments: Map, + attachments: ReadonlyMap, ) { const undeclaredAttachments = Array.from(attachments.entries()).filter( ([inputName]) => inputMap[inputName] === undefined, @@ -117,7 +99,7 @@ function resolveInputs( /** @internal */ export function createAppNodeInstance(options: { spec: AppNodeSpec; - attachments: Map; + attachments: ReadonlyMap; }): AppNodeInstance { const { spec, attachments } = options; const { id, extension, config, source } = spec; From 2b1ceb2fe152410fa52a27f8dc5eacf62fbd84be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 11:59:44 +0200 Subject: [PATCH 084/141] frontend-app-api: move app node instance creation into instantiateAppNodeTree Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/createAppNodeInstance.ts | 155 ---------------- .../wiring/graph/instantiateAppNodeTree.ts | 170 ++++++++++++++++-- .../src/wiring/graph/types.ts | 5 - 3 files changed, 159 insertions(+), 171 deletions(-) delete mode 100644 packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts diff --git a/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts deleted file mode 100644 index 22cb3d4df7..0000000000 --- a/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2023 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 { - AnyExtensionDataMap, - AnyExtensionInputMap, - ExtensionDataRef, -} from '@backstage/frontend-plugin-api'; -import mapValues from 'lodash/mapValues'; -import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; - -type AppNodeWithInstance = AppNode & { instance: AppNodeInstance }; - -function resolveInputData( - dataMap: AnyExtensionDataMap, - attachment: AppNodeWithInstance, - inputName: string, -) { - return mapValues(dataMap, ref => { - const value = attachment.instance.getData(ref); - if (value === undefined && !ref.config.optional) { - throw new Error( - `input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.spec.id}'`, - ); - } - return value; - }); -} - -function resolveInputs( - inputMap: AnyExtensionInputMap, - attachments: ReadonlyMap, -) { - const undeclaredAttachments = Array.from(attachments.entries()).filter( - ([inputName]) => inputMap[inputName] === undefined, - ); - // TODO: Make this a warning rather than an error - if (undeclaredAttachments.length > 0) { - throw new Error( - `received undeclared input${ - undeclaredAttachments.length > 1 ? 's' : '' - } ${undeclaredAttachments - .map( - ([k, exts]) => - `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts - .map(e => e.spec.id) - .join("', '")}'`, - ) - .join(' and ')}`, - ); - } - - return mapValues(inputMap, (input, inputName) => { - const attachedNodes = - attachments - .get(inputName) - ?.filter((node): node is AppNodeWithInstance => - Boolean(node.instance), - ) ?? []; - - if (input.config.singleton) { - if (attachedNodes.length > 1) { - const attachedNodeIds = attachedNodes.map(e => e.spec.id); - throw Error( - `expected ${ - input.config.optional ? 'at most' : 'exactly' - } one '${inputName}' input but received multiple: '${attachedNodeIds.join( - "', '", - )}'`, - ); - } else if (attachedNodes.length === 0) { - if (input.config.optional) { - return undefined; - } - throw Error(`input '${inputName}' is required but was not received`); - } - return resolveInputData(input.extensionData, attachedNodes[0], inputName); - } - - return attachedNodes.map(attachment => - resolveInputData(input.extensionData, attachment, inputName), - ); - }); -} - -/** @internal */ -export function createAppNodeInstance(options: { - spec: AppNodeSpec; - attachments: ReadonlyMap; -}): AppNodeInstance { - const { spec, attachments } = options; - const { id, extension, config, source } = spec; - const extensionData = new Map(); - const extensionDataRefs = new Set>(); - - let parsedConfig: unknown; - try { - parsedConfig = extension.configSchema?.parse(config ?? {}); - } catch (e) { - throw new Error( - `Invalid configuration for extension '${id}'; caused by ${e}`, - ); - } - - try { - extension.factory({ - source, - config: parsedConfig, - bind: namedOutputs => { - for (const [name, output] of Object.entries(namedOutputs)) { - const ref = extension.output[name]; - if (!ref) { - throw new Error(`unknown output provided via '${name}'`); - } - if (extensionData.has(ref.id)) { - throw new Error( - `duplicate extension data '${ref.id}' received via output '${name}'`, - ); - } - extensionData.set(ref.id, output); - extensionDataRefs.add(ref); - } - }, - inputs: resolveInputs(extension.inputs, attachments), - }); - } catch (e) { - throw new Error( - `Failed to instantiate extension '${id}'${ - e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` - }`, - ); - } - - return { - getDataRefs() { - return extensionDataRefs.values(); - }, - getData(ref: ExtensionDataRef): T | undefined { - return extensionData.get(ref.id) as T | undefined; - }, - }; -} diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts index b01ad020b9..ba0c85692e 100644 --- a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts @@ -14,30 +14,178 @@ * limitations under the License. */ -import { createAppNodeInstance } from './createAppNodeInstance'; -import { AppNode, Mutable } from './types'; +import { + AnyExtensionDataMap, + AnyExtensionInputMap, + ExtensionDataRef, +} from '@backstage/frontend-plugin-api'; +import mapValues from 'lodash/mapValues'; +import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; +type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +function resolveInputData( + dataMap: AnyExtensionDataMap, + attachment: { id: string; instance: AppNodeInstance }, + inputName: string, +) { + return mapValues(dataMap, ref => { + const value = attachment.instance.getData(ref); + if (value === undefined && !ref.config.optional) { + throw new Error( + `input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.id}'`, + ); + } + return value; + }); +} + +function resolveInputs( + inputMap: AnyExtensionInputMap, + attachments: ReadonlyMap, +) { + const undeclaredAttachments = Array.from(attachments.entries()).filter( + ([inputName]) => inputMap[inputName] === undefined, + ); + // TODO: Make this a warning rather than an error + if (undeclaredAttachments.length > 0) { + throw new Error( + `received undeclared input${ + undeclaredAttachments.length > 1 ? 's' : '' + } ${undeclaredAttachments + .map( + ([k, exts]) => + `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts + .map(e => e.id) + .join("', '")}'`, + ) + .join(' and ')}`, + ); + } + + return mapValues(inputMap, (input, inputName) => { + const attachedNodes = attachments.get(inputName) ?? []; + + if (input.config.singleton) { + if (attachedNodes.length > 1) { + const attachedNodeIds = attachedNodes.map(e => e.id); + throw Error( + `expected ${ + input.config.optional ? 'at most' : 'exactly' + } one '${inputName}' input but received multiple: '${attachedNodeIds.join( + "', '", + )}'`, + ); + } else if (attachedNodes.length === 0) { + if (input.config.optional) { + return undefined; + } + throw Error(`input '${inputName}' is required but was not received`); + } + return resolveInputData(input.extensionData, attachedNodes[0], inputName); + } + + return attachedNodes.map(attachment => + resolveInputData(input.extensionData, attachment, inputName), + ); + }); +} + +/** @internal */ +export function createAppNodeInstance(options: { + spec: AppNodeSpec; + attachments: ReadonlyMap; +}): AppNodeInstance { + const { spec, attachments } = options; + const { id, extension, config, source } = spec; + const extensionData = new Map(); + const extensionDataRefs = new Set>(); + + let parsedConfig: unknown; + try { + parsedConfig = extension.configSchema?.parse(config ?? {}); + } catch (e) { + throw new Error( + `Invalid configuration for extension '${id}'; caused by ${e}`, + ); + } + + try { + extension.factory({ + source, + config: parsedConfig, + bind: namedOutputs => { + for (const [name, output] of Object.entries(namedOutputs)) { + const ref = extension.output[name]; + if (!ref) { + throw new Error(`unknown output provided via '${name}'`); + } + if (extensionData.has(ref.id)) { + throw new Error( + `duplicate extension data '${ref.id}' received via output '${name}'`, + ); + } + extensionData.set(ref.id, output); + extensionDataRefs.add(ref); + } + }, + inputs: resolveInputs(extension.inputs, attachments), + }); + } catch (e) { + throw new Error( + `Failed to instantiate extension '${id}'${ + e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` + }`, + ); + } + + return { + getDataRefs() { + return extensionDataRefs.values(); + }, + getData(ref: ExtensionDataRef): T | undefined { + return extensionData.get(ref.id) as T | undefined; + }, + }; +} + +/** + * Starting at the provided node, instantiate all reachable nodes in the graph that have not been disabled. + * @internal + */ export function instantiateAppNodeTree(rootNode: AppNode): void { - function createInstance(node: AppNode): void { + function createInstance(node: AppNode): AppNodeInstance | undefined { if (node.instance) { - return; + return node.instance; } if (node.spec.disabled) { - return; + return undefined; } - for (const children of node.edges.attachments.values()) { - for (const child of children) { - createInstance(child); - } + const instantiatedAttachments = new Map< + string, + { id: string; instance: AppNodeInstance }[] + >(); + + for (const [input, children] of node.edges.attachments) { + const instantiatedChildren = children.flatMap(child => { + const childInstance = createInstance(child); + if (!childInstance) { + return []; + } + return [{ id: child.spec.id, instance: childInstance }]; + }); + instantiatedAttachments.set(input, instantiatedChildren); } (node as Mutable).instance = createAppNodeInstance({ spec: node.spec, - attachments: node.edges.attachments, + attachments: instantiatedAttachments, }); - return; + return node.instance; } createInstance(rootNode); diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index d7f80faa1b..b828c9762e 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -20,11 +20,6 @@ import { ExtensionDataRef, } from '@backstage/frontend-plugin-api'; -/** @internal */ -export type Mutable = { - -readonly [P in keyof T]: T[P]; -}; - /** * The specification for this node in the app graph. * @public From aded5d1ce4a47ed9284e2559f41c6dff27270343 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 12:29:32 +0200 Subject: [PATCH 085/141] frontend-app-api: migrate createExtensionInstance tests Signed-off-by: Patrik Oldsberg --- .../graph/createExtensionInstance.test.ts | 456 ------------------ .../graph/instantiateAppNodeTree.test.ts | 441 +++++++++++++++++ 2 files changed, 441 insertions(+), 456 deletions(-) delete mode 100644 packages/frontend-app-api/src/wiring/graph/createExtensionInstance.test.ts create mode 100644 packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts diff --git a/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.test.ts b/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.test.ts deleted file mode 100644 index 6e69e99cb7..0000000000 --- a/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.test.ts +++ /dev/null @@ -1,456 +0,0 @@ -/* - * Copyright 2023 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 { - createExtension, - createExtensionDataRef, - createExtensionInput, - createSchemaFromZod, -} from '@backstage/frontend-plugin-api'; -import { createExtensionInstance } from './createExtensionInstance'; - -const testDataRef = createExtensionDataRef('test'); -const otherDataRef = createExtensionDataRef('other'); -const inputMirrorDataRef = createExtensionDataRef('mirror'); - -const simpleExtension = createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - other: otherDataRef.optional(), - }, - configSchema: createSchemaFromZod(z => - z.object({ - output: z.string().default('test'), - other: z.number().optional(), - }), - ), - factory({ bind, config }) { - bind({ test: config.output, other: config.other }); - }, -}); - -describe('createExtensionInstance', () => { - it('should create a simple extension instance', () => { - const attachments = new Map(); - const instance = createExtensionInstance({ - attachments, - config: undefined, - extension: simpleExtension, - }); - - expect(instance.id).toBe('core.test'); - expect(instance.attachments).toBe(attachments); - expect(instance.getData(testDataRef)).toEqual('test'); - }); - - it('should create an extension with different kind of inputs', () => { - const attachments = new Map([ - [ - 'optionalSingletonPresent', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'optionalSingletonPresent' }, - extension: simpleExtension, - }), - ], - ], - [ - 'singleton', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'singleton', other: 2 }, - extension: simpleExtension, - }), - ], - ], - [ - 'many', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many2', other: 3 }, - extension: simpleExtension, - }), - ], - ], - ]); - const instance = createExtensionInstance({ - attachments, - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - optionalSingletonPresent: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true, optional: true }, - ), - optionalSingletonMissing: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true, optional: true }, - ), - singleton: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true }, - ), - many: createExtensionInput({ - test: testDataRef, - other: otherDataRef.optional(), - }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ bind, inputs }) { - bind({ inputMirror: inputs }); - }, - }), - }); - - expect(instance.id).toBe('core.test'); - expect(instance.attachments).toBe(attachments); - expect(instance.getData(inputMirrorDataRef)).toEqual({ - optionalSingletonPresent: { test: 'optionalSingletonPresent' }, - singleton: { test: 'singleton', other: 2 }, - many: [{ test: 'many1' }, { test: 'many2', other: 3 }], - }); - }); - - it('should refuse to create an extension with invalid config', () => { - expect(() => - createExtensionInstance({ - attachments: new Map(), - config: { other: 'not-a-number' }, - extension: simpleExtension, - }), - ).toThrow( - "Invalid configuration for extension 'core.test'; caused by Error: Expected number, received string at 'other'", - ); - }); - - it('should forward extension factory errors', () => { - expect(() => - createExtensionInstance({ - attachments: new Map(), - config: { other: 'not-a-number' }, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory() { - const error = new Error('NOPE'); - error.name = 'NopeError'; - throw error; - }, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test'; caused by NopeError: NOPE", - ); - }); - - it('should refuse to create an instance with duplicate output', () => { - const attachments = new Map(); - expect(() => - createExtensionInstance({ - attachments, - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test1: testDataRef, - test2: testDataRef, - }, - factory({ bind }) { - bind({ test1: 'test', test2: 'test2' }); - }, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', duplicate extension data 'test' received via output 'test2'", - ); - }); - - it('should refuse to create an instance with disconnected output data', () => { - const attachments = new Map(); - expect(() => - createExtensionInstance({ - attachments, - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - }, - factory({ bind }) { - bind({ nonexistent: 'test' } as any); - }, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', unknown output provided via 'nonexistent'", - ); - }); - - it('should refuse to create an instance with missing required input', () => { - expect(() => - createExtensionInstance({ - attachments: new Map(), - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory() {}, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', input 'singleton' is required but was not received", - ); - }); - - it('should refuse to create an instance with undeclared inputs', () => { - expect(() => - createExtensionInstance({ - attachments: new Map([ - [ - 'declared', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - ], - ], - [ - 'undeclared', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - ], - ], - ]), - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - declared: createExtensionInput({ - test: testDataRef, - }), - }, - output: {}, - factory() {}, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', received undeclared input 'undeclared' from extension 'core.test'", - ); - }); - - it('should refuse to create an instance with multiple undeclared inputs', () => { - expect(() => - createExtensionInstance({ - attachments: new Map([ - [ - 'undeclared1', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - ], - ], - [ - 'undeclared2', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - ], - ], - ]), - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory() {}, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', received undeclared inputs 'undeclared1' from extension 'core.test' and 'undeclared2' from extensions 'core.test', 'core.test'", - ); - }); - - it('should refuse to create an instance with multiple inputs for required singleton', () => { - expect(() => - createExtensionInstance({ - attachments: new Map([ - [ - 'singleton', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many2' }, - extension: simpleExtension, - }), - ], - ], - ]), - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory() {}, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', expected exactly one 'singleton' input but received multiple: 'core.test', 'core.test'", - ); - }); - - it('should refuse to create an instance with multiple inputs for optional singleton', () => { - expect(() => - createExtensionInstance({ - attachments: new Map([ - [ - 'singleton', - [ - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many1' }, - extension: simpleExtension, - }), - createExtensionInstance({ - attachments: new Map(), - config: { output: 'many2' }, - extension: simpleExtension, - }), - ], - ], - ]), - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, - }, - { singleton: true, optional: true }, - ), - }, - output: {}, - factory() {}, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', expected at most one 'singleton' input but received multiple: 'core.test', 'core.test'", - ); - }); - - it('should refuse to create an instance with multiple inputs that did not provide required data', () => { - expect(() => - createExtensionInstance({ - attachments: new Map([ - [ - 'singleton', - [ - createExtensionInstance({ - attachments: new Map(), - config: undefined, - extension: simpleExtension, - }), - ], - ], - ]), - config: undefined, - extension: createExtension({ - id: 'core.test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - other: otherDataRef, - }, - { singleton: true }, - ), - }, - output: {}, - factory() {}, - }), - }), - ).toThrow( - "Failed to instantiate extension 'core.test', input 'singleton' did not receive required extension data 'other' from extension 'core.test'", - ); - }); -}); diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts new file mode 100644 index 0000000000..ddd842c6c0 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts @@ -0,0 +1,441 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Extension, + createExtension, + createExtensionDataRef, + createExtensionInput, + createSchemaFromZod, +} from '@backstage/frontend-plugin-api'; +import { createAppNodeInstance } from './instantiateAppNodeTree'; +import { AppNodeInstance, AppNodeSpec } from './types'; + +const testDataRef = createExtensionDataRef('test'); +const otherDataRef = createExtensionDataRef('other'); +const inputMirrorDataRef = createExtensionDataRef('mirror'); + +const simpleExtension = createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + other: otherDataRef.optional(), + }, + configSchema: createSchemaFromZod(z => + z.object({ + output: z.string().default('test'), + other: z.number().optional(), + }), + ), + factory({ bind, config }) { + bind({ test: config.output, other: config.other }); + }, +}); + +function makeSpec( + extension: Extension, + config?: TConfig, +): AppNodeSpec { + return { + id: extension.id, + attachTo: extension.attachTo, + disabled: extension.disabled, + extension, + config, + source: undefined, + }; +} + +function makeInstanceWithId( + extension: Extension, + config?: TConfig, +): { id: string; instance: AppNodeInstance } { + return { + id: extension.id, + instance: createAppNodeInstance({ + spec: makeSpec(extension, config), + attachments: new Map(), + }), + }; +} + +describe('createAppNodeInstance', () => { + it('should create a simple extension instance', () => { + const attachments = new Map(); + const instance = createAppNodeInstance({ + spec: makeSpec(simpleExtension), + attachments, + }); + + expect(Array.from(instance.getDataRefs())).toEqual([ + testDataRef, + otherDataRef.optional(), + ]); + expect(instance.getData(testDataRef)).toEqual('test'); + }); + + it('should create an extension with different kind of inputs', () => { + const attachments = new Map([ + [ + 'optionalSingletonPresent', + [ + makeInstanceWithId(simpleExtension, { + output: 'optionalSingletonPresent', + }), + ], + ], + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { + output: 'singleton', + other: 2, + }), + ], + ], + [ + 'many', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many2', other: 3 }), + ], + ], + ]); + const instance = createAppNodeInstance({ + attachments, + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + optionalSingletonPresent: createExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true, optional: true }, + ), + optionalSingletonMissing: createExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true, optional: true }, + ), + singleton: createExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true }, + ), + many: createExtensionInput({ + test: testDataRef, + other: otherDataRef.optional(), + }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ bind, inputs }) { + bind({ inputMirror: inputs }); + }, + }), + ), + }); + + expect(Array.from(instance.getDataRefs())).toEqual([inputMirrorDataRef]); + expect(instance.getData(inputMirrorDataRef)).toEqual({ + optionalSingletonPresent: { test: 'optionalSingletonPresent' }, + singleton: { test: 'singleton', other: 2 }, + many: [{ test: 'many1' }, { test: 'many2', other: 3 }], + }); + }); + + it('should refuse to create an extension with invalid config', () => { + expect(() => + createAppNodeInstance({ + spec: { + ...makeSpec(simpleExtension), + config: { other: 'not-a-number' }, + }, + attachments: new Map(), + }), + ).toThrow( + "Invalid configuration for extension 'core.test'; caused by Error: Expected number, received string at 'other'", + ); + }); + + it('should forward extension factory errors', () => { + expect(() => + createAppNodeInstance({ + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, + }), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'core.test'; caused by NopeError: NOPE", + ); + }); + + it('should refuse to create an instance with duplicate output', () => { + expect(() => + createAppNodeInstance({ + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test1: testDataRef, + test2: testDataRef, + }, + factory({ bind }) { + bind({ test1: 'test', test2: 'test2' }); + }, + }), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', duplicate extension data 'test' received via output 'test2'", + ); + }); + + it('should refuse to create an instance with disconnected output data', () => { + expect(() => + createAppNodeInstance({ + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + }, + factory({ bind }) { + bind({ nonexistent: 'test' } as any); + }, + }), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', unknown output provided via 'nonexistent'", + ); + }); + + it('should refuse to create an instance with missing required input', () => { + expect(() => + createAppNodeInstance({ + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory() {}, + }), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', input 'singleton' is required but was not received", + ); + }); + + it('should refuse to create an instance with undeclared inputs', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'declared', + [ + makeInstanceWithId(simpleExtension, { + output: 'many1', + }), + ], + ], + [ + 'undeclared', + [ + makeInstanceWithId(simpleExtension, { + output: 'many1', + }), + ], + ], + ]), + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + declared: createExtensionInput({ + test: testDataRef, + }), + }, + output: {}, + factory() {}, + }), + ), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', received undeclared input 'undeclared' from extension 'core.test'", + ); + }); + + it('should refuse to create an instance with multiple undeclared inputs', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'undeclared1', + [makeInstanceWithId(simpleExtension, { output: 'many1' })], + ], + [ + 'undeclared2', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many1' }), + ], + ], + ]), + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory() {}, + }), + ), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', received undeclared inputs 'undeclared1' from extension 'core.test' and 'undeclared2' from extensions 'core.test', 'core.test'", + ); + }); + + it('should refuse to create an instance with multiple inputs for required singleton', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many2' }), + ], + ], + ]), + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory() {}, + }), + ), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', expected exactly one 'singleton' input but received multiple: 'core.test', 'core.test'", + ); + }); + + it('should refuse to create an instance with multiple inputs for optional singleton', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many2' }), + ], + ], + ]), + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + test: testDataRef, + }, + { singleton: true, optional: true }, + ), + }, + output: {}, + factory() {}, + }), + ), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', expected at most one 'singleton' input but received multiple: 'core.test', 'core.test'", + ); + }); + + it('should refuse to create an instance with multiple inputs that did not provide required data', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], + ]), + spec: makeSpec( + createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + other: otherDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory() {}, + }), + ), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', input 'singleton' did not receive required extension data 'other' from extension 'core.test'", + ); + }); +}); From d844ec8a0be4c6d67c06110f18e12b11f503c594 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 12:52:17 +0200 Subject: [PATCH 086/141] frontend-app-api: add tests for instantiateAppNodeTree Signed-off-by: Patrik Oldsberg --- .../graph/instantiateAppNodeTree.test.ts | 123 +++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts index ddd842c6c0..43aad4f9ae 100644 --- a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts @@ -21,8 +21,12 @@ import { createExtensionInput, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; -import { createAppNodeInstance } from './instantiateAppNodeTree'; +import { + createAppNodeInstance, + instantiateAppNodeTree, +} from './instantiateAppNodeTree'; import { AppNodeInstance, AppNodeSpec } from './types'; +import { buildAppGraph } from './buildAppGraph'; const testDataRef = createExtensionDataRef('test'); const otherDataRef = createExtensionDataRef('other'); @@ -73,6 +77,123 @@ function makeInstanceWithId( }; } +describe('instantiateAppNodeTree', () => { + it('should instantiate a single node', () => { + const graph = buildAppGraph( + [{ ...makeSpec(simpleExtension), id: 'root-node' }], + 'root-node', + ); + expect(graph.root.instance).not.toBeDefined(); + instantiateAppNodeTree(graph.root); + expect(graph.root.instance).toBeDefined(); + expect(graph.root.instance?.getData(testDataRef)).toBe('test'); + + // Multiple calls should have no effect + instantiateAppNodeTree(graph.root); + expect(graph.root.instance).toBeDefined(); + }); + + it('should not instantiate disabled nodes', () => { + const graph = buildAppGraph( + [{ ...makeSpec(simpleExtension), id: 'root-node', disabled: true }], + 'root-node', + ); + expect(graph.root.instance).not.toBeDefined(); + instantiateAppNodeTree(graph.root); + expect(graph.root.instance).not.toBeDefined(); + }); + + it('should instantiate a node with attachments', () => { + const graph = buildAppGraph( + [ + { + ...makeSpec( + createExtension({ + id: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ bind, inputs }) { + bind({ inputMirror: inputs }); + }, + }), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }, + ], + 'root-node', + ); + + const childNode = graph.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(graph.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(graph.root); + expect(graph.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + expect(graph.root.instance?.getData(inputMirrorDataRef)).toEqual({ + test: [{ test: 'test' }], + }); + + // Multiple calls should have no effect + instantiateAppNodeTree(graph.root); + expect(graph.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + }); + + it('should not instantiate disabled attachments', () => { + const graph = buildAppGraph( + [ + { + ...makeSpec( + createExtension({ + id: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ bind, inputs }) { + bind({ inputMirror: inputs }); + }, + }), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + disabled: true, + }, + ], + 'root-node', + ); + + const childNode = graph.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(graph.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(graph.root); + expect(graph.root.instance).toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + expect(graph.root.instance?.getData(inputMirrorDataRef)).toEqual({ + test: [], + }); + }); +}); + describe('createAppNodeInstance', () => { it('should create a simple extension instance', () => { const attachments = new Map(); From 65b6a918944c7120b7ae69c218002b0e6699f6c5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:02:12 +0200 Subject: [PATCH 087/141] frontend-app-api: rename buildAppGraph -> resolveAppGraph Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/instantiateAppNodeTree.test.ts | 10 +++++----- ...buildAppGraph.test.ts => resolveAppGraph.test.ts} | 12 ++++++------ .../graph/{buildAppGraph.ts => resolveAppGraph.ts} | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) rename packages/frontend-app-api/src/wiring/graph/{buildAppGraph.test.ts => resolveAppGraph.test.ts} (93%) rename packages/frontend-app-api/src/wiring/graph/{buildAppGraph.ts => resolveAppGraph.ts} (99%) diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts index 43aad4f9ae..7bffc69c7c 100644 --- a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts @@ -26,7 +26,7 @@ import { instantiateAppNodeTree, } from './instantiateAppNodeTree'; import { AppNodeInstance, AppNodeSpec } from './types'; -import { buildAppGraph } from './buildAppGraph'; +import { resolveAppGraph } from './resolveAppGraph'; const testDataRef = createExtensionDataRef('test'); const otherDataRef = createExtensionDataRef('other'); @@ -79,7 +79,7 @@ function makeInstanceWithId( describe('instantiateAppNodeTree', () => { it('should instantiate a single node', () => { - const graph = buildAppGraph( + const graph = resolveAppGraph( [{ ...makeSpec(simpleExtension), id: 'root-node' }], 'root-node', ); @@ -94,7 +94,7 @@ describe('instantiateAppNodeTree', () => { }); it('should not instantiate disabled nodes', () => { - const graph = buildAppGraph( + const graph = resolveAppGraph( [{ ...makeSpec(simpleExtension), id: 'root-node', disabled: true }], 'root-node', ); @@ -104,7 +104,7 @@ describe('instantiateAppNodeTree', () => { }); it('should instantiate a node with attachments', () => { - const graph = buildAppGraph( + const graph = resolveAppGraph( [ { ...makeSpec( @@ -151,7 +151,7 @@ describe('instantiateAppNodeTree', () => { }); it('should not instantiate disabled attachments', () => { - const graph = buildAppGraph( + const graph = resolveAppGraph( [ { ...makeSpec( diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts b/packages/frontend-app-api/src/wiring/graph/resolveAppGraph.test.ts similarity index 93% rename from packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts rename to packages/frontend-app-api/src/wiring/graph/resolveAppGraph.test.ts index 8cf8279e96..2d1acbbc04 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/resolveAppGraph.test.ts @@ -15,7 +15,7 @@ */ import { createExtension } from '@backstage/frontend-plugin-api'; -import { buildAppGraph } from './buildAppGraph'; +import { resolveAppGraph } from './resolveAppGraph'; const extBaseConfig = { id: 'test', @@ -34,13 +34,13 @@ const baseSpec = { describe('buildAppGraph', () => { it('should fail to create an empty graph', () => { - expect(() => buildAppGraph([])).toThrow( + expect(() => resolveAppGraph([])).toThrow( "No root node with id 'core' found in app graph", ); }); it('should create a graph with only one node', () => { - const graph = buildAppGraph([{ ...baseSpec, id: 'core' }]); + const graph = resolveAppGraph([{ ...baseSpec, id: 'core' }]); expect(graph.root).toEqual({ spec: { ...baseSpec, id: 'core' }, edges: { attachments: new Map() }, @@ -50,7 +50,7 @@ describe('buildAppGraph', () => { }); it('should create a graph', () => { - const graph = buildAppGraph( + const graph = resolveAppGraph( [ { ...baseSpec, id: 'a' }, { ...baseSpec, id: 'b' }, @@ -116,7 +116,7 @@ describe('buildAppGraph', () => { }); it('should create a graph out of order', () => { - const graph = buildAppGraph( + const graph = resolveAppGraph( [ { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, { ...baseSpec, id: 'a' }, @@ -163,7 +163,7 @@ describe('buildAppGraph', () => { it('throws an error when duplicated extensions are detected', () => { expect(() => - buildAppGraph([ + resolveAppGraph([ { ...baseSpec, id: 'a' }, { ...baseSpec, id: 'a' }, ]), diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/resolveAppGraph.ts similarity index 99% rename from packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts rename to packages/frontend-app-api/src/wiring/graph/resolveAppGraph.ts index 06347b1ece..18f481fa30 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts +++ b/packages/frontend-app-api/src/wiring/graph/resolveAppGraph.ts @@ -87,7 +87,7 @@ class SerializableAppNode implements AppNode { * tree with all attachments in the same order as they appear in the input specs array. * @internal */ -export function buildAppGraph( +export function resolveAppGraph( specs: AppNodeSpec[], rootNodeId = 'core', ): AppGraph { From 90b9ef567d88a2127568a39a6d4303f06dadb142 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:05:48 +0200 Subject: [PATCH 088/141] frontend-app-api: add createAppGraph Signed-off-by: Patrik Oldsberg --- .../src/wiring/graph/createAppGraph.ts | 47 +++++++++++++++++++ .../src/wiring/graph/index.ts | 1 + 2 files changed, 48 insertions(+) create mode 100644 packages/frontend-app-api/src/wiring/graph/createAppGraph.ts diff --git a/packages/frontend-app-api/src/wiring/graph/createAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/createAppGraph.ts new file mode 100644 index 0000000000..4c08b7c5b4 --- /dev/null +++ b/packages/frontend-app-api/src/wiring/graph/createAppGraph.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2023 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 { + BackstagePlugin, + Extension, + ExtensionOverrides, +} from '@backstage/frontend-plugin-api'; +import { readAppExtensionsConfig } from './readAppExtensionsConfig'; +import { resolveAppGraph } from './resolveAppGraph'; +import { resolveAppNodeSpecs } from './resolveAppNodeSpecs'; +import { AppGraph } from './types'; +import { Config } from '@backstage/config'; +import { instantiateAppNodeTree } from './instantiateAppNodeTree'; + +/** @internal */ +export interface CreateAppGraphOptions { + features: (BackstagePlugin | ExtensionOverrides)[]; + builtinExtensions: Extension[]; + config: Config; +} + +/** @internal */ +export function createAppGraph(options: CreateAppGraphOptions): AppGraph { + const appGraph = resolveAppGraph( + resolveAppNodeSpecs({ + features: options.features, + builtinExtensions: options.builtinExtensions, + parameters: readAppExtensionsConfig(options.config), + }), + ); + instantiateAppNodeTree(appGraph.root); + return appGraph; +} diff --git a/packages/frontend-app-api/src/wiring/graph/index.ts b/packages/frontend-app-api/src/wiring/graph/index.ts index 608ecb7675..4a3b54dffb 100644 --- a/packages/frontend-app-api/src/wiring/graph/index.ts +++ b/packages/frontend-app-api/src/wiring/graph/index.ts @@ -20,3 +20,4 @@ export type { AppNodeInstance, AppNodeSpec, } from './types'; +export { createAppGraph } from './createAppGraph'; From eb7c037e6fb93a782a367fb3d48e1ac45007328e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:07:15 +0200 Subject: [PATCH 089/141] frontend-app-api: fix AppNodeInstance.getData return type Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/wiring/graph/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index b828c9762e..6e205db9dd 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -48,7 +48,7 @@ export interface AppNodeEdges { */ export interface AppNodeInstance { getDataRefs(): Iterable>; - getData(ref: ExtensionDataRef): T | unknown; + getData(ref: ExtensionDataRef): T | undefined; } /** From fd6dd7c079ad4f82377e3cf3a27c56a35c50c285 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:13:08 +0200 Subject: [PATCH 090/141] frontend-app-api: migrate extractRouteInfoFromInstanceTree to app graph Signed-off-by: Patrik Oldsberg --- .../extractRouteInfoFromInstanceTree.test.ts | 11 ++++++++--- .../extractRouteInfoFromInstanceTree.ts | 18 +++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts index 0de33b50f2..d990d998b9 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts @@ -27,8 +27,12 @@ import { createPlugin, createRouteRef, } from '@backstage/frontend-plugin-api'; -import { createInstances } from '../wiring/createApp'; import { MockConfigApi } from '@backstage/test-utils'; +import { createAppGraph } from '../wiring/graph'; +import { Core } from '../extensions/Core'; +import { CoreRoutes } from '../extensions/CoreRoutes'; +import { CoreNav } from '../extensions/CoreNav'; +import { CoreLayout } from '../extensions/CoreLayout'; const ref1 = createRouteRef(); const ref2 = createRouteRef(); @@ -73,12 +77,13 @@ function routeInfoFromExtensions(extensions: Extension[]) { id: 'test', extensions, }); - const { coreInstance } = createInstances({ + const graph = createAppGraph({ config: new MockConfigApi({}), + builtinExtensions: [Core, CoreRoutes, CoreNav, CoreLayout], features: [plugin], }); - return extractRouteInfoFromInstanceTree(coreInstance); + return extractRouteInfoFromInstanceTree(graph.root); } function sortedEntries(map: Map): [RouteRef, T][] { diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts index fadcb719b9..16e0e7d8ec 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts @@ -15,10 +15,10 @@ */ import { RouteRef, coreExtensionData } from '@backstage/frontend-plugin-api'; -import { ExtensionInstance } from '../wiring/graph/createExtensionInstance'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toLegacyPlugin } from '../wiring/createApp'; import { BackstageRouteObject } from './types'; +import { AppNode } from '../wiring/graph'; // We always add a child that matches all subroutes but without any route refs. This makes // sure that we're always able to match each route no matter how deep the navigation goes. @@ -41,7 +41,7 @@ export function joinPaths(...paths: string[]): string { return normalized; } -export function extractRouteInfoFromInstanceTree(core: ExtensionInstance): { +export function extractRouteInfoFromInstanceTree(core: AppNode): { routePaths: Map; routeParents: Map; routeObjects: BackstageRouteObject[]; @@ -56,17 +56,17 @@ export function extractRouteInfoFromInstanceTree(core: ExtensionInstance): { const routeObjects = new Array(); function visit( - current: ExtensionInstance, + current: AppNode, collectedPath?: string, foundRefForCollectedPath: boolean = false, parentRef?: RouteRef, candidateParentRef?: RouteRef, parentObj?: BackstageRouteObject, ) { - const routePath = current - .getData(coreExtensionData.routePath) + const routePath = current.instance + ?.getData(coreExtensionData.routePath) ?.replace(/^\//, ''); - const routeRef = current.getData(coreExtensionData.routeRef); + const routeRef = current.instance?.getData(coreExtensionData.routeRef); const parentChildren = parentObj?.children ?? routeObjects; let currentObj = parentObj; @@ -124,12 +124,12 @@ export function extractRouteInfoFromInstanceTree(core: ExtensionInstance): { routeParents.set(routeRef, newParentRef); currentObj?.routeRefs.add(routeRef); - if (current.source) { - currentObj?.plugins.add(toLegacyPlugin(current.source)); + if (current.spec.source) { + currentObj?.plugins.add(toLegacyPlugin(current.spec.source)); } } - for (const children of current.attachments.values()) { + for (const children of current.edges.attachments.values()) { for (const child of children) { visit( child, From ea262277488df2e3b5b27dea9bfe42948176b381 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:17:09 +0200 Subject: [PATCH 091/141] frontend-app-api: rename extractRouteInfoFromInstanceTree -> extractRouteInfoFromAppNode Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/routing/RouteResolver.test.ts | 2 +- ...stanceTree.test.ts => extractRouteInfoFromAppNode.test.ts} | 4 ++-- ...InfoFromInstanceTree.ts => extractRouteInfoFromAppNode.ts} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/frontend-app-api/src/routing/{extractRouteInfoFromInstanceTree.test.ts => extractRouteInfoFromAppNode.test.ts} (98%) rename packages/frontend-app-api/src/routing/{extractRouteInfoFromInstanceTree.ts => extractRouteInfoFromAppNode.ts} (98%) diff --git a/packages/frontend-app-api/src/routing/RouteResolver.test.ts b/packages/frontend-app-api/src/routing/RouteResolver.test.ts index e37ee14929..81e9d10be4 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.test.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.test.ts @@ -24,7 +24,7 @@ import { } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { RouteResolver } from './RouteResolver'; -import { MATCH_ALL_ROUTE } from './extractRouteInfoFromInstanceTree'; +import { MATCH_ALL_ROUTE } from './extractRouteInfoFromAppNode'; const rest = { element: null, diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts similarity index 98% rename from packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts rename to packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index d990d998b9..2eab3c8e7a 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -16,7 +16,7 @@ import React from 'react'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { extractRouteInfoFromInstanceTree } from './extractRouteInfoFromInstanceTree'; +import { extractRouteInfoFromAppNode } from './extractRouteInfoFromAppNode'; import { AnyRouteRefParams, Extension, @@ -83,7 +83,7 @@ function routeInfoFromExtensions(extensions: Extension[]) { features: [plugin], }); - return extractRouteInfoFromInstanceTree(graph.root); + return extractRouteInfoFromAppNode(graph.root); } function sortedEntries(map: Map): [RouteRef, T][] { diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts similarity index 98% rename from packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts rename to packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts index 16e0e7d8ec..1ecb28113b 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts @@ -41,7 +41,7 @@ export function joinPaths(...paths: string[]): string { return normalized; } -export function extractRouteInfoFromInstanceTree(core: AppNode): { +export function extractRouteInfoFromAppNode(node: AppNode): { routePaths: Map; routeParents: Map; routeObjects: BackstageRouteObject[]; @@ -143,7 +143,7 @@ export function extractRouteInfoFromInstanceTree(core: AppNode): { } } - visit(core); + visit(node); return { routePaths, routeParents, routeObjects }; } From fcf1b80beda4edbcffc1b906b50d69e47f9eb219 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:26:56 +0200 Subject: [PATCH 092/141] frontend-app-api: migrate createApp to use createAppGraph Signed-off-by: Patrik Oldsberg --- .../frontend-app-api/src/wiring/createApp.tsx | 150 +++++------------- 1 file changed, 44 insertions(+), 106 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 485882c8eb..1dbfb3912b 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -28,11 +28,6 @@ import { Core } from '../extensions/Core'; import { CoreRoutes } from '../extensions/CoreRoutes'; import { CoreLayout } from '../extensions/CoreLayout'; import { CoreNav } from '../extensions/CoreNav'; -import { - createExtensionInstance, - ExtensionInstance, -} from './graph/createExtensionInstance'; -import { resolveAppNodeSpecs } from './graph/resolveAppNodeSpecs'; import { AnyApiFactory, ApiHolder, @@ -82,7 +77,7 @@ import { import { BrowserRouter, Route } from 'react-router-dom'; import { SidebarItem } from '@backstage/core-components'; import { DarkTheme, LightTheme } from '../extensions/themes'; -import { extractRouteInfoFromInstanceTree } from '../routing/extractRouteInfoFromInstanceTree'; +import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode'; import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; import { appLanguageApiRef, @@ -92,8 +87,16 @@ import { AppRouteBinder } from '../routing'; import { RoutingProvider } from '../routing/RoutingProvider'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; -import { readAppExtensionsConfig } from './graph/readAppExtensionsConfig'; -import { AppNodeSpec } from './graph'; +import { AppNode, createAppGraph } from './graph'; + +const builtinExtensions = [ + Core, + CoreRoutes, + CoreNav, + CoreLayout, + LightTheme, + DarkTheme, +]; /** @public */ export interface ExtensionTreeNode { @@ -114,20 +117,38 @@ export function createExtensionTree(options: { config: Config; }): ExtensionTree { const features = getAvailableFeatures(options.config); - const { instances } = createInstances({ + const graph = createAppGraph({ features, + builtinExtensions, config: options.config, }); + function convertNode(node?: AppNode): ExtensionTreeNode | undefined { + return ( + node && { + id: node.spec.id, + getData(ref: ExtensionDataRef): T | undefined { + return node.instance?.getData(ref); + }, + } + ); + } + return { getExtension(id: string): ExtensionTreeNode | undefined { - return instances.get(id); + return convertNode(graph.nodes.get(id)); }, getExtensionAttachments( id: string, inputName: string, ): ExtensionTreeNode[] { - return instances.get(id)?.attachments.get(inputName) ?? []; + return ( + graph.nodes + .get(id) + ?.edges.attachments.get(inputName) + ?.map(convertNode) + .filter((node): node is ExtensionTreeNode => Boolean(node)) ?? [] + ); }, getRootRoutes(): JSX.Element[] { return this.getExtensionAttachments('core.routes', 'routes').map(node => { @@ -177,89 +198,6 @@ export function createExtensionTree(options: { }; } -/** - * @internal - */ -export function createInstances(options: { - features: (BackstagePlugin | ExtensionOverrides)[]; - config: Config; -}) { - const builtinExtensions = [ - Core, - CoreRoutes, - CoreNav, - CoreLayout, - LightTheme, - DarkTheme, - ]; - - // pull in default extension instance from discovered packages - // apply config to adjust default extension instances and add more - const appNodeSpecs = resolveAppNodeSpecs({ - features: options.features, - builtinExtensions, - parameters: readAppExtensionsConfig(options.config), - }); - - // TODO: validate the config of all extension instances - // We do it at this point to ensure that merging (if any) of config has already happened - - // Create attachment map so that we can look attachments up during instance creation - const attachmentMap = new Map>(); - for (const instanceParams of appNodeSpecs) { - const extensionId = instanceParams.attachTo.id; - const pointId = instanceParams.attachTo.input; - let pointMap = attachmentMap.get(extensionId); - if (!pointMap) { - pointMap = new Map(); - attachmentMap.set(extensionId, pointMap); - } - - let instances = pointMap.get(pointId); - if (!instances) { - instances = []; - pointMap.set(pointId, instances); - } - - instances.push(instanceParams); - } - - const instances = new Map(); - - function createInstance(instanceParams: AppNodeSpec): ExtensionInstance { - const extensionId = instanceParams.extension.id; - const existingInstance = instances.get(extensionId); - if (existingInstance) { - return existingInstance; - } - - const attachments = new Map( - Array.from(attachmentMap.get(extensionId)?.entries() ?? []).map( - ([inputName, attachmentConfigs]) => { - return [inputName, attachmentConfigs.map(createInstance)]; - }, - ), - ); - - const newInstance = createExtensionInstance({ - extension: instanceParams.extension, - source: instanceParams.source, - config: instanceParams.config, - attachments, - }); - - instances.set(extensionId, newInstance); - - return newInstance; - } - - const coreInstance = createInstance( - appNodeSpecs.find(p => p.extension.id === 'core')!, - ); - - return { coreInstance, instances }; -} - function deduplicateFeatures( allFeatures: (BackstagePlugin | ExtensionOverrides)[], ): (BackstagePlugin | ExtensionOverrides)[] { @@ -309,8 +247,9 @@ export function createApp(options: { ...(options.features ?? []), ]); - const { coreInstance } = createInstances({ + const appGraph = createAppGraph({ features: allFeatures, + builtinExtensions, config, }); @@ -323,11 +262,11 @@ export function createApp(options: { const routeIds = collectRouteIds(allFeatures); const App = () => ( - + {/* TODO: set base path using the logic from AppRouter */} - {coreInstance.getData(coreExtensionData.reactElement)} + {appGraph.root.instance!.getData( + coreExtensionData.reactElement, + )} @@ -417,22 +358,19 @@ function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { }; } -function createApiHolder( - coreExtension: ExtensionInstance, - configApi: ConfigApi, -): ApiHolder { +function createApiHolder(core: AppNode, configApi: ConfigApi): ApiHolder { const factoryRegistry = new ApiFactoryRegistry(); const pluginApis = - coreExtension.attachments + core.edges.attachments .get('apis') - ?.map(e => e.getData(coreExtensionData.apiFactory)) + ?.map(e => e.instance?.getData(coreExtensionData.apiFactory)) .filter((x): x is AnyApiFactory => !!x) ?? []; const themeExtensions = - coreExtension.attachments + core.edges.attachments .get('themes') - ?.map(e => e.getData(coreExtensionData.theme)) + ?.map(e => e.instance?.getData(coreExtensionData.theme)) .filter((x): x is AppTheme => !!x) ?? []; for (const factory of [...defaultApis, ...pluginApis]) { From 6473b37182161d76e0a3b006dae7ef42a87ce8f4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:28:54 +0200 Subject: [PATCH 093/141] frontend-app-api: move graph/ to top level Signed-off-by: Patrik Oldsberg --- .../frontend-app-api/src/{wiring => }/graph/createAppGraph.ts | 0 packages/frontend-app-api/src/{wiring => }/graph/index.ts | 0 .../src/{wiring => }/graph/instantiateAppNodeTree.test.ts | 0 .../src/{wiring => }/graph/instantiateAppNodeTree.ts | 0 .../src/{wiring => }/graph/readAppExtensionsConfig.test.ts | 0 .../src/{wiring => }/graph/readAppExtensionsConfig.ts | 0 .../src/{wiring => }/graph/resolveAppGraph.test.ts | 0 .../frontend-app-api/src/{wiring => }/graph/resolveAppGraph.ts | 0 .../src/{wiring => }/graph/resolveAppNodeSpecs.test.ts | 0 .../src/{wiring => }/graph/resolveAppNodeSpecs.ts | 2 +- packages/frontend-app-api/src/{wiring => }/graph/types.ts | 0 .../src/routing/extractRouteInfoFromAppNode.test.ts | 2 +- .../frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts | 2 +- packages/frontend-app-api/src/wiring/createApp.tsx | 2 +- 14 files changed, 4 insertions(+), 4 deletions(-) rename packages/frontend-app-api/src/{wiring => }/graph/createAppGraph.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/index.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/instantiateAppNodeTree.test.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/instantiateAppNodeTree.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/readAppExtensionsConfig.test.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/readAppExtensionsConfig.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/resolveAppGraph.test.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/resolveAppGraph.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/resolveAppNodeSpecs.test.ts (100%) rename packages/frontend-app-api/src/{wiring => }/graph/resolveAppNodeSpecs.ts (98%) rename packages/frontend-app-api/src/{wiring => }/graph/types.ts (100%) diff --git a/packages/frontend-app-api/src/wiring/graph/createAppGraph.ts b/packages/frontend-app-api/src/graph/createAppGraph.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/createAppGraph.ts rename to packages/frontend-app-api/src/graph/createAppGraph.ts diff --git a/packages/frontend-app-api/src/wiring/graph/index.ts b/packages/frontend-app-api/src/graph/index.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/index.ts rename to packages/frontend-app-api/src/graph/index.ts diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/graph/instantiateAppNodeTree.test.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.test.ts rename to packages/frontend-app-api/src/graph/instantiateAppNodeTree.test.ts diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/graph/instantiateAppNodeTree.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts rename to packages/frontend-app-api/src/graph/instantiateAppNodeTree.ts diff --git a/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.test.ts b/packages/frontend-app-api/src/graph/readAppExtensionsConfig.test.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.test.ts rename to packages/frontend-app-api/src/graph/readAppExtensionsConfig.test.ts diff --git a/packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.ts b/packages/frontend-app-api/src/graph/readAppExtensionsConfig.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/readAppExtensionsConfig.ts rename to packages/frontend-app-api/src/graph/readAppExtensionsConfig.ts diff --git a/packages/frontend-app-api/src/wiring/graph/resolveAppGraph.test.ts b/packages/frontend-app-api/src/graph/resolveAppGraph.test.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/resolveAppGraph.test.ts rename to packages/frontend-app-api/src/graph/resolveAppGraph.test.ts diff --git a/packages/frontend-app-api/src/wiring/graph/resolveAppGraph.ts b/packages/frontend-app-api/src/graph/resolveAppGraph.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/resolveAppGraph.ts rename to packages/frontend-app-api/src/graph/resolveAppGraph.ts diff --git a/packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.test.ts rename to packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts diff --git a/packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts similarity index 98% rename from packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.ts rename to packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts index 8cad54f4be..51480117d7 100644 --- a/packages/frontend-app-api/src/wiring/graph/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts @@ -20,7 +20,7 @@ import { ExtensionOverrides, } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { toInternalExtensionOverrides } from '../../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; +import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; import { ExtensionParameters } from './readAppExtensionsConfig'; import { AppNodeSpec } from './types'; diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/graph/types.ts similarity index 100% rename from packages/frontend-app-api/src/wiring/graph/types.ts rename to packages/frontend-app-api/src/graph/types.ts diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index 2eab3c8e7a..e0adf3062a 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -28,7 +28,7 @@ import { createRouteRef, } from '@backstage/frontend-plugin-api'; import { MockConfigApi } from '@backstage/test-utils'; -import { createAppGraph } from '../wiring/graph'; +import { createAppGraph } from '../graph'; import { Core } from '../extensions/Core'; import { CoreRoutes } from '../extensions/CoreRoutes'; import { CoreNav } from '../extensions/CoreNav'; diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts index 1ecb28113b..71991ee44e 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts @@ -18,7 +18,7 @@ import { RouteRef, coreExtensionData } from '@backstage/frontend-plugin-api'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toLegacyPlugin } from '../wiring/createApp'; import { BackstageRouteObject } from './types'; -import { AppNode } from '../wiring/graph'; +import { AppNode } from '../graph'; // We always add a child that matches all subroutes but without any route refs. This makes // sure that we're always able to match each route no matter how deep the navigation goes. diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 1dbfb3912b..4f7817314a 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -87,7 +87,7 @@ import { AppRouteBinder } from '../routing'; import { RoutingProvider } from '../routing/RoutingProvider'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; -import { AppNode, createAppGraph } from './graph'; +import { AppNode, createAppGraph } from '../graph'; const builtinExtensions = [ Core, From 6d2258621f5e3ed4ef98e18d5478ca0e3ad9600d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:36:39 +0200 Subject: [PATCH 094/141] frontend-app-api: update resolveAppNodeSpecs tests Signed-off-by: Patrik Oldsberg --- .../src/graph/resolveAppNodeSpecs.test.ts | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts index 1d3c0a26ed..a3cda23113 100644 --- a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts @@ -54,8 +54,18 @@ describe('resolveAppNodeSpecs', () => { parameters: [], }), ).toEqual([ - { extension: a, attachTo: { id: 'root', input: 'default' } }, - { extension: b, attachTo: { id: 'root', input: 'default' } }, + { + id: 'a', + extension: a, + attachTo: { id: 'root', input: 'default' }, + disabled: false, + }, + { + id: 'b', + extension: b, + attachTo: { id: 'root', input: 'default' }, + disabled: false, + }, ]); }); @@ -76,11 +86,18 @@ describe('resolveAppNodeSpecs', () => { }), ).toEqual([ { + id: 'a', extension: a, attachTo: { id: 'root', input: 'default' }, source: pluginA, + disabled: false, + }, + { + id: 'b', + extension: b, + attachTo: { id: 'derp', input: 'default' }, + disabled: false, }, - { extension: b, attachTo: { id: 'derp', input: 'default' } }, ]); }); @@ -109,16 +126,20 @@ describe('resolveAppNodeSpecs', () => { }), ).toEqual([ { + id: 'a', extension: a, attachTo: { id: 'root', input: 'default' }, source: plugin, config: { foo: { bar: 1 } }, + disabled: false, }, { + id: 'b', extension: b, attachTo: { id: 'root', input: 'default' }, source: plugin, config: { foo: { qux: 3 } }, + disabled: false, }, ]); }); @@ -142,8 +163,18 @@ describe('resolveAppNodeSpecs', () => { ], }), ).toEqual([ - { extension: b, attachTo: { id: 'root', input: 'default' } }, - { extension: a, attachTo: { id: 'root', input: 'default' } }, + { + id: 'b', + extension: b, + attachTo: { id: 'root', input: 'default' }, + disabled: false, + }, + { + id: 'a', + extension: a, + attachTo: { id: 'root', input: 'default' }, + disabled: false, + }, ]); }); @@ -173,10 +204,12 @@ describe('resolveAppNodeSpecs', () => { expect(result[0].source).toBe(plugin); expect(result[1]).toEqual({ + id: 'c', extension: cOverride, attachTo: { id: 'root', input: 'default' }, config: undefined, source: undefined, + disabled: false, }); }); From 5aad4f7720080956f4e646f8d029e14410f513bd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 13:37:07 +0200 Subject: [PATCH 095/141] frontend-app-api: no longer reject core extension configuration Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts index 51480117d7..d6cfcaede2 100644 --- a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts @@ -164,13 +164,6 @@ export function resolveAppNodeSpecs(options: { for (const overrideParam of parameters) { const extensionId = overrideParam.id; - // Prevent core parametrization - if (extensionId === 'core') { - throw new Error( - "A 'core' extension configuration was detected, but the core extension is not configurable", - ); - } - const existingIndex = configuredExtensions.findIndex( e => e.extension.id === extensionId, ); From 89dc1250d86138f73b4b7e927e55c56b326291bc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 19:18:10 +0200 Subject: [PATCH 096/141] frontend-app-api: make it possible to configure which extensions are forbidden Signed-off-by: Patrik Oldsberg --- .../src/graph/createAppGraph.ts | 2 + .../src/graph/resolveAppNodeSpecs.test.ts | 41 +++++++++++++++++++ .../src/graph/resolveAppNodeSpecs.ts | 28 ++++++++----- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/frontend-app-api/src/graph/createAppGraph.ts b/packages/frontend-app-api/src/graph/createAppGraph.ts index 4c08b7c5b4..cb72fa3cd1 100644 --- a/packages/frontend-app-api/src/graph/createAppGraph.ts +++ b/packages/frontend-app-api/src/graph/createAppGraph.ts @@ -40,7 +40,9 @@ export function createAppGraph(options: CreateAppGraphOptions): AppGraph { features: options.features, builtinExtensions: options.builtinExtensions, parameters: readAppExtensionsConfig(options.config), + forbidden: new Set(['core']), }), + 'core', ); instantiateAppNodeTree(appGraph.root); return appGraph; diff --git a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts index a3cda23113..59930968c6 100644 --- a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.test.ts @@ -234,4 +234,45 @@ describe('resolveAppNodeSpecs', () => { expect(result.map(r => r.extension.id)).toEqual(['b', 'c', 'a']); }); + + it('throws an error when a forbidden extension is overridden by a plugin', () => { + expect(() => + resolveAppNodeSpecs({ + features: [ + createPlugin({ id: 'test', extensions: [makeExt('forbidden')] }), + ], + builtinExtensions: [], + parameters: [], + forbidden: new Set(['forbidden']), + }), + ).toThrow( + "It is forbidden to override the following extension(s): 'forbidden', which is done by the following plugin(s): 'test'", + ); + }); + + it('throws an error when a forbidden extension is overridden by overrides', () => { + expect(() => + resolveAppNodeSpecs({ + features: [ + createExtensionOverrides({ extensions: [makeExt('forbidden')] }), + ], + builtinExtensions: [], + parameters: [], + forbidden: new Set(['forbidden']), + }), + ).toThrow( + "It is forbidden to override the following extension(s): 'forbidden', which is done by one or more extension overrides", + ); + }); + + it('throws an error when a forbidden extension is parametrized', () => { + expect(() => + resolveAppNodeSpecs({ + features: [], + builtinExtensions: [], + parameters: [{ id: 'forbidden', disabled: false }], + forbidden: new Set(['forbidden']), + }), + ).toThrow("Configuration of the 'forbidden' extension is forbidden"); + }); }); diff --git a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts index d6cfcaede2..ca3c4fd1ea 100644 --- a/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/graph/resolveAppNodeSpecs.ts @@ -29,8 +29,9 @@ export function resolveAppNodeSpecs(options: { features: (BackstagePlugin | ExtensionOverrides)[]; builtinExtensions: Extension[]; parameters: Array; + forbidden?: Set; }): AppNodeSpec[] { - const { builtinExtensions, parameters } = options; + const { builtinExtensions, parameters, forbidden = new Set() } = options; const plugins = options.features.filter( (f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin', @@ -48,20 +49,21 @@ export function resolveAppNodeSpecs(options: { ); // Prevent core override - if (pluginExtensions.some(({ id }) => id === 'core')) { - const pluginIds = pluginExtensions - .filter(({ id }) => id === 'core') - .map(({ source }) => source.id); + if (pluginExtensions.some(({ id }) => forbidden.has(id))) { + const pluginsStr = pluginExtensions + .filter(({ id }) => forbidden.has(id)) + .map(({ source }) => `'${source.id}'`) + .join(', '); + const forbiddenStr = [...forbidden].map(id => `'${id}'`).join(', '); throw new Error( - `The following plugin(s) are overriding the 'core' extension which is forbidden: ${pluginIds.join( - ',', - )}`, + `It is forbidden to override the following extension(s): ${forbiddenStr}, which is done by the following plugin(s): ${pluginsStr}`, ); } - if (overrideExtensions.some(({ id }) => id === 'root')) { + if (overrideExtensions.some(({ id }) => forbidden.has(id))) { + const forbiddenStr = [...forbidden].map(id => `'${id}'`).join(', '); throw new Error( - `An extension override is overriding the 'root' extension which is forbidden`, + `It is forbidden to override the following extension(s): ${forbiddenStr}, which is done by one or more extension overrides`, ); } const overrideExtensionIds = overrideExtensions.map(({ id }) => id); @@ -164,6 +166,12 @@ export function resolveAppNodeSpecs(options: { for (const overrideParam of parameters) { const extensionId = overrideParam.id; + if (forbidden.has(extensionId)) { + throw new Error( + `Configuration of the '${extensionId}' extension is forbidden`, + ); + } + const existingIndex = configuredExtensions.findIndex( e => e.extension.id === extensionId, ); From ee39751fdffa80b0a6f0535ea520863887958fcf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 19:20:12 +0200 Subject: [PATCH 097/141] frontend-app-api: make rootNodeId required in resolveAppGraph Signed-off-by: Patrik Oldsberg --- .../src/graph/createAppGraph.ts | 2 +- .../src/graph/instantiateAppNodeTree.test.ts | 118 ++++++++---------- .../src/graph/resolveAppGraph.test.ts | 48 ++++--- .../src/graph/resolveAppGraph.ts | 2 +- 4 files changed, 78 insertions(+), 92 deletions(-) diff --git a/packages/frontend-app-api/src/graph/createAppGraph.ts b/packages/frontend-app-api/src/graph/createAppGraph.ts index cb72fa3cd1..ace8595f53 100644 --- a/packages/frontend-app-api/src/graph/createAppGraph.ts +++ b/packages/frontend-app-api/src/graph/createAppGraph.ts @@ -36,13 +36,13 @@ export interface CreateAppGraphOptions { /** @internal */ export function createAppGraph(options: CreateAppGraphOptions): AppGraph { const appGraph = resolveAppGraph( + 'core', resolveAppNodeSpecs({ features: options.features, builtinExtensions: options.builtinExtensions, parameters: readAppExtensionsConfig(options.config), forbidden: new Set(['core']), }), - 'core', ); instantiateAppNodeTree(appGraph.root); return appGraph; diff --git a/packages/frontend-app-api/src/graph/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/graph/instantiateAppNodeTree.test.ts index 7bffc69c7c..21f364dda2 100644 --- a/packages/frontend-app-api/src/graph/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/graph/instantiateAppNodeTree.test.ts @@ -79,10 +79,9 @@ function makeInstanceWithId( describe('instantiateAppNodeTree', () => { it('should instantiate a single node', () => { - const graph = resolveAppGraph( - [{ ...makeSpec(simpleExtension), id: 'root-node' }], - 'root-node', - ); + const graph = resolveAppGraph('root-node', [ + { ...makeSpec(simpleExtension), id: 'root-node' }, + ]); expect(graph.root.instance).not.toBeDefined(); instantiateAppNodeTree(graph.root); expect(graph.root.instance).toBeDefined(); @@ -94,43 +93,39 @@ describe('instantiateAppNodeTree', () => { }); it('should not instantiate disabled nodes', () => { - const graph = resolveAppGraph( - [{ ...makeSpec(simpleExtension), id: 'root-node', disabled: true }], - 'root-node', - ); + const graph = resolveAppGraph('root-node', [ + { ...makeSpec(simpleExtension), id: 'root-node', disabled: true }, + ]); expect(graph.root.instance).not.toBeDefined(); instantiateAppNodeTree(graph.root); expect(graph.root.instance).not.toBeDefined(); }); it('should instantiate a node with attachments', () => { - const graph = resolveAppGraph( - [ - { - ...makeSpec( - createExtension({ - id: 'root-node', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createExtensionInput({ test: testDataRef }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ bind, inputs }) { - bind({ inputMirror: inputs }); - }, - }), - ), - }, - { - ...makeSpec(simpleExtension), - id: 'child-node', - attachTo: { id: 'root-node', input: 'test' }, - }, - ], - 'root-node', - ); + const graph = resolveAppGraph('root-node', [ + { + ...makeSpec( + createExtension({ + id: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ bind, inputs }) { + bind({ inputMirror: inputs }); + }, + }), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }, + ]); const childNode = graph.nodes.get('child-node'); expect(childNode).toBeDefined(); @@ -151,34 +146,31 @@ describe('instantiateAppNodeTree', () => { }); it('should not instantiate disabled attachments', () => { - const graph = resolveAppGraph( - [ - { - ...makeSpec( - createExtension({ - id: 'root-node', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createExtensionInput({ test: testDataRef }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ bind, inputs }) { - bind({ inputMirror: inputs }); - }, - }), - ), - }, - { - ...makeSpec(simpleExtension), - id: 'child-node', - attachTo: { id: 'root-node', input: 'test' }, - disabled: true, - }, - ], - 'root-node', - ); + const graph = resolveAppGraph('root-node', [ + { + ...makeSpec( + createExtension({ + id: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ bind, inputs }) { + bind({ inputMirror: inputs }); + }, + }), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + disabled: true, + }, + ]); const childNode = graph.nodes.get('child-node'); expect(childNode).toBeDefined(); diff --git a/packages/frontend-app-api/src/graph/resolveAppGraph.test.ts b/packages/frontend-app-api/src/graph/resolveAppGraph.test.ts index 2d1acbbc04..2668c51863 100644 --- a/packages/frontend-app-api/src/graph/resolveAppGraph.test.ts +++ b/packages/frontend-app-api/src/graph/resolveAppGraph.test.ts @@ -34,13 +34,13 @@ const baseSpec = { describe('buildAppGraph', () => { it('should fail to create an empty graph', () => { - expect(() => resolveAppGraph([])).toThrow( + expect(() => resolveAppGraph('core', [])).toThrow( "No root node with id 'core' found in app graph", ); }); it('should create a graph with only one node', () => { - const graph = resolveAppGraph([{ ...baseSpec, id: 'core' }]); + const graph = resolveAppGraph('core', [{ ...baseSpec, id: 'core' }]); expect(graph.root).toEqual({ spec: { ...baseSpec, id: 'core' }, edges: { attachments: new Map() }, @@ -50,18 +50,15 @@ describe('buildAppGraph', () => { }); it('should create a graph', () => { - const graph = resolveAppGraph( - [ - { ...baseSpec, id: 'a' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ], - 'b', - ); + const graph = resolveAppGraph('b', [ + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ]); expect(Array.from(graph.nodes.keys())).toEqual([ 'a', @@ -116,18 +113,15 @@ describe('buildAppGraph', () => { }); it('should create a graph out of order', () => { - const graph = resolveAppGraph( - [ - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, id: 'a' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ], - 'b', - ); + const graph = resolveAppGraph('b', [ + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, id: 'a' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ]); expect(Array.from(graph.nodes.keys())).toEqual([ 'bx2', @@ -163,7 +157,7 @@ describe('buildAppGraph', () => { it('throws an error when duplicated extensions are detected', () => { expect(() => - resolveAppGraph([ + resolveAppGraph('core', [ { ...baseSpec, id: 'a' }, { ...baseSpec, id: 'a' }, ]), diff --git a/packages/frontend-app-api/src/graph/resolveAppGraph.ts b/packages/frontend-app-api/src/graph/resolveAppGraph.ts index 18f481fa30..354b4c53ee 100644 --- a/packages/frontend-app-api/src/graph/resolveAppGraph.ts +++ b/packages/frontend-app-api/src/graph/resolveAppGraph.ts @@ -88,8 +88,8 @@ class SerializableAppNode implements AppNode { * @internal */ export function resolveAppGraph( + rootNodeId: string, specs: AppNodeSpec[], - rootNodeId = 'core', ): AppGraph { const nodes = new Map(); From d2e2b01ae1fa93b21eb66d0ba1cf4b655f6228a8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 19:25:52 +0200 Subject: [PATCH 098/141] frontend-app-api: move some createApp tests to createAppGraph Signed-off-by: Patrik Oldsberg --- .../src/graph/createAppGraph.test.ts | 123 +++++++++++ .../src/wiring/createApp.test.tsx | 194 +----------------- 2 files changed, 124 insertions(+), 193 deletions(-) create mode 100644 packages/frontend-app-api/src/graph/createAppGraph.test.ts diff --git a/packages/frontend-app-api/src/graph/createAppGraph.test.ts b/packages/frontend-app-api/src/graph/createAppGraph.test.ts new file mode 100644 index 0000000000..2c9f062199 --- /dev/null +++ b/packages/frontend-app-api/src/graph/createAppGraph.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2023 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 { + createExtension, + createExtensionOverrides, + createPlugin, +} from '@backstage/frontend-plugin-api'; +import { MockConfigApi } from '@backstage/test-utils'; +import { createAppGraph } from './createAppGraph'; + +const extBase = { + id: 'test', + attachTo: { id: 'core', input: 'root' }, + output: {}, + factory() {}, +}; + +describe('createAppGraph', () => { + it('throws an error when a core extension is parametrized', () => { + const config = new MockConfigApi({ + app: { + extensions: [ + { + core: {}, + }, + ], + }, + }); + const features = [ + createPlugin({ + id: 'plugin', + extensions: [], + }), + ]; + expect(() => + createAppGraph({ features, config, builtinExtensions: [] }), + ).toThrow("Configuration of the 'core' extension is forbidden"); + }); + + it('throws an error when a core extension is overridden', () => { + const config = new MockConfigApi({}); + const features = [ + createPlugin({ + id: 'plugin', + extensions: [ + createExtension({ + id: 'core', + attachTo: { id: 'core.routes', input: 'route' }, + inputs: {}, + output: {}, + factory() {}, + }), + ], + }), + ]; + expect(() => + createAppGraph({ features, config, builtinExtensions: [] }), + ).toThrow( + "It is forbidden to override the following extension(s): 'core', which is done by the following plugin(s): 'plugin'", + ); + }); + + it('throws an error when duplicated extensions are detected', () => { + const config = new MockConfigApi({}); + + const ExtensionA = createExtension({ ...extBase, id: 'A' }); + + const ExtensionB = createExtension({ ...extBase, id: 'B' }); + + const PluginA = createPlugin({ + id: 'A', + extensions: [ExtensionA, ExtensionA], + }); + + const PluginB = createPlugin({ + id: 'B', + extensions: [ExtensionA, ExtensionB, ExtensionB], + }); + + const features = [PluginA, PluginB]; + + expect(() => + createAppGraph({ features, config, builtinExtensions: [] }), + ).toThrow( + "The following extensions are duplicated: The extension 'A' was provided 2 time(s) by the plugin 'A' and 1 time(s) by the plugin 'B', The extension 'B' was provided 2 time(s) by the plugin 'B'", + ); + }); + + it('throws an error when duplicated extension overrides are detected', () => { + expect(() => + createAppGraph({ + features: [ + createExtensionOverrides({ + extensions: [ + createExtension({ ...extBase, id: 'a' }), + createExtension({ ...extBase, id: 'a' }), + createExtension({ ...extBase, id: 'b' }), + ], + }), + createExtensionOverrides({ + extensions: [createExtension({ ...extBase, id: 'b' })], + }), + ], + config: new MockConfigApi({}), + builtinExtensions: [], + }), + ).toThrow('The following extensions had duplicate overrides: a, b'); + }); +}); diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index de574bc6a9..e9fb48f957 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -15,123 +15,15 @@ */ import { - createExtension, - createExtensionOverrides, createPageExtension, createPlugin, - createRouteRef, createThemeExtension, } from '@backstage/frontend-plugin-api'; -import { createApp, createInstances } from './createApp'; import { screen, waitFor } from '@testing-library/react'; +import { createApp } from './createApp'; import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import React from 'react'; -const extBaseConfig = { - id: 'test', - attachTo: { id: 'root', input: 'default' }, - output: {}, - factory() {}, -}; - -describe('createInstances', () => { - it('throws an error when a core extension is parametrized', () => { - const config = new MockConfigApi({ - app: { - extensions: [ - { - core: {}, - }, - ], - }, - }); - const features = [ - createPlugin({ - id: 'plugin', - extensions: [], - }), - ]; - expect(() => createInstances({ config, features })).toThrow( - "A 'core' extension configuration was detected, but the core extension is not configurable", - ); - }); - - it('throws an error when a core extension is overridden', () => { - const config = new MockConfigApi({}); - const features = [ - createPlugin({ - id: 'plugin', - extensions: [ - createExtension({ - id: 'core', - attachTo: { id: 'core.routes', input: 'route' }, - inputs: {}, - output: {}, - factory() {}, - }), - ], - }), - ]; - expect(() => createInstances({ config, features })).toThrow( - "The following plugin(s) are overriding the 'core' extension which is forbidden: plugin", - ); - }); - - it('throws an error when duplicated extensions are detected', () => { - const config = new MockConfigApi({}); - - const ExtensionA = createPageExtension({ - id: 'A', - defaultPath: '/', - routeRef: createRouteRef(), - loader: async () =>
Extension A
, - }); - - const ExtensionB = createPageExtension({ - id: 'B', - defaultPath: '/', - routeRef: createRouteRef(), - loader: async () =>
Extension B
, - }); - - const PluginA = createPlugin({ - id: 'A', - extensions: [ExtensionA, ExtensionA], - }); - - const PluginB = createPlugin({ - id: 'B', - extensions: [ExtensionA, ExtensionB, ExtensionB], - }); - - const features = [PluginA, PluginB]; - - expect(() => createInstances({ config, features })).toThrow( - "The following extensions are duplicated: The extension 'A' was provided 2 time(s) by the plugin 'A' and 1 time(s) by the plugin 'B', The extension 'B' was provided 2 time(s) by the plugin 'B'", - ); - }); - - it('throws an error when duplicated extension overrides are detected', () => { - expect(() => - createInstances({ - config: new MockConfigApi({}), - features: [ - createExtensionOverrides({ - extensions: [ - createExtension({ ...extBaseConfig, id: 'a' }), - createExtension({ ...extBaseConfig, id: 'a' }), - createExtension({ ...extBaseConfig, id: 'b' }), - ], - }), - createExtensionOverrides({ - extensions: [createExtension({ ...extBaseConfig, id: 'b' })], - }), - ], - }), - ).toThrow('The following extensions had duplicate overrides: a, b'); - }); -}); - describe('createApp', () => { it('should allow themes to be installed', async () => { const app = createApp({ @@ -161,90 +53,6 @@ describe('createApp', () => { await expect(screen.findByText('Derp')).resolves.toBeInTheDocument(); }); - it('should log an app', () => { - const { coreInstance } = createInstances({ - config: new MockConfigApi({}), - features: [], - }); - - expect(String(coreInstance)).toMatchInlineSnapshot(` - " - root [ - - content [ - - ] - nav [ - - ] - - ] - themes [ - - - ] - " - `); - }); - - it('should serialize an app as JSON', () => { - const { coreInstance } = createInstances({ - config: new MockConfigApi({}), - features: [], - }); - - expect(JSON.parse(JSON.stringify(coreInstance))).toMatchInlineSnapshot(` - { - "attachments": { - "root": [ - { - "attachments": { - "content": [ - { - "id": "core.routes", - "output": [ - "core.reactElement", - ], - }, - ], - "nav": [ - { - "id": "core.nav", - "output": [ - "core.reactElement", - ], - }, - ], - }, - "id": "core.layout", - "output": [ - "core.reactElement", - ], - }, - ], - "themes": [ - { - "id": "themes.light", - "output": [ - "core.theme", - ], - }, - { - "id": "themes.dark", - "output": [ - "core.theme", - ], - }, - ], - }, - "id": "core", - "output": [ - "core.reactElement", - ], - } - `); - }); - it('should deduplicate features keeping the last received one', async () => { const duplicatedFeatureId = 'test'; const app = createApp({ From 062f7a85cbf8e6612f4b1255ee8321e25720fa1e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 21:46:37 +0200 Subject: [PATCH 099/141] frontend-app-api: add graph types docs Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/graph/types.ts | 38 +++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/frontend-app-api/src/graph/types.ts b/packages/frontend-app-api/src/graph/types.ts index 6e205db9dd..9267f13158 100644 --- a/packages/frontend-app-api/src/graph/types.ts +++ b/packages/frontend-app-api/src/graph/types.ts @@ -20,9 +20,18 @@ import { ExtensionDataRef, } from '@backstage/frontend-plugin-api'; +/* +NOTE: These types are marked as @internal for now, but the intention is for this to be a public API in the future. +*/ + /** * The specification for this node in the app graph. - * @public + * + * @internal + * @remarks + * + * The specifications for a collection of app nodes is all the information needed + * to build the graph and instantiate the nodes. */ export interface AppNodeSpec { readonly id: string; @@ -35,7 +44,12 @@ export interface AppNodeSpec { /** * The connections from this node to other nodes. - * @public + * + * @internal + * @remarks + * + * The app node edges are resolved based on the app node specs, regardless of whether + * adjacent nodes are disabled or not. If no parent attachment is present or */ export interface AppNodeEdges { readonly attachedTo?: { node: AppNode; input: string }; @@ -44,16 +58,24 @@ export interface AppNodeEdges { /** * The instance of this node in the app graph. - * @public + * + * @internal + * @remarks + * + * The app node instance is created when the `factory` function of an extension is called. + * Instances will only be present for nodes in the app that are connected to the root + * node and not disabled */ export interface AppNodeInstance { + /** Returns a sequence of all extension data refs that were output by this instance */ getDataRefs(): Iterable>; + /** Get the output data for a single extension data ref */ getData(ref: ExtensionDataRef): T | undefined; } /** * - * @public + * @internal */ export interface AppNode { /** The specification for how this node should be instantiated */ @@ -64,8 +86,16 @@ export interface AppNode { readonly instance?: AppNodeInstance; } +/** + * The app graph containing all nodes of the app. + * + * @internal + */ export interface AppGraph { + /** The root node of the app */ root: AppNode; + /** A map of all nodes in the app by ID, including orphaned or disabled nodes */ nodes: ReadonlyMap; + /** A sequence of all nodes with a parent that is not reachable from the app root node */ orphans: Iterable; } From e28d379e32f9506d9cf67d65add82bba25534ce7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 00:17:37 +0200 Subject: [PATCH 100/141] changesets: add changeset for frontend-app-api app graph refactor Signed-off-by: Patrik Oldsberg --- .changeset/good-plums-confess.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/good-plums-confess.md diff --git a/.changeset/good-plums-confess.md b/.changeset/good-plums-confess.md new file mode 100644 index 0000000000..941adec8b3 --- /dev/null +++ b/.changeset/good-plums-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Refactor internal extension instance system into an app graph. From 1f0b6b1e48b0002a2b6ef3515ac7927c38494f1e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 19:06:01 +0200 Subject: [PATCH 101/141] search: more reliable wait in test Signed-off-by: Patrik Oldsberg --- .../search/src/components/SearchModal/SearchModal.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 274f9e21a1..6fce560800 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; @@ -203,9 +203,9 @@ describe('SearchModal', () => { expect.objectContaining({ term: 'term' }), ); - const input = screen.getByLabelText('Search'); + const input = screen.getByLabelText('Search'); await userEvent.clear(input); - await 'a tick'; + await waitFor(() => expect(input.value).toBe('')); await userEvent.type(input, 'new term{enter}'); expect(navigate).toHaveBeenCalledWith('/search?query=new term'); From 22ca64f117c29a50301ca7c56436720c2c10b1db Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 19 Oct 2023 12:02:23 -0400 Subject: [PATCH 102/141] fix(configLoader): properly resolve targets into absolute paths Signed-off-by: Phil Kuang --- .changeset/chatty-cobras-cheer.md | 5 +++++ .../config-loader/src/sources/ConfigSources.test.ts | 12 ++++++++++-- packages/config-loader/src/sources/ConfigSources.ts | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 .changeset/chatty-cobras-cheer.md diff --git a/.changeset/chatty-cobras-cheer.md b/.changeset/chatty-cobras-cheer.md new file mode 100644 index 0000000000..c8203e58c0 --- /dev/null +++ b/.changeset/chatty-cobras-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Correctly resolve config targets into absolute paths diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index 43feb3ca27..f095bbec42 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -95,6 +95,14 @@ describe('ConfigSources', () => { ), ).toEqual([{ name: 'FileConfigSource', path: '/config.yaml' }]); + expect( + mergeSources( + ConfigSources.defaultForTargets({ + targets: [{ type: 'path', target: 'config.yaml' }], + }), + ), + ).toEqual([{ name: 'FileConfigSource', path: resolvePath('config.yaml') }]); + const subFunc = async () => undefined; expect( mergeSources( @@ -172,8 +180,8 @@ describe('ConfigSources', () => { }), ), ).toEqual([ - { name: 'FileConfigSource', path: 'a.yaml' }, - { name: 'FileConfigSource', path: 'b.yaml' }, + { name: 'FileConfigSource', path: resolvePath('a.yaml') }, + { name: 'FileConfigSource', path: resolvePath('b.yaml') }, { name: 'EnvConfigSource', env: { HOME: '/' } }, ]); }); diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index 43123f87ea..03fc6d83d6 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -161,7 +161,7 @@ export class ConfigSources { } return FileConfigSource.create({ watch: options.watch, - path: arg.target, + path: resolvePath(arg.target), substitutionFunc: options.substitutionFunc, }); }); From 39cd2d7a1f9e2831b00e2ec3229e8df14af592f3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 17:02:33 +0200 Subject: [PATCH 103/141] starting point for entity page extension API Co-authored-by: Philipp Hugenroth Co-authored-by: Vincenzo Scamporlino Co-authored-by: Camila Belo Co-authored-by: Rickard Dybeck Co-authored-by: Ben Lambert Co-authored-by: Jack Palmer Co-authored-by: Elon Jefferson Signed-off-by: Patrik Oldsberg --- plugins/catalog/src/alpha.tsx | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 31551c1930..0753737740 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -283,3 +283,51 @@ export default createPlugin({ CatalogNavItem, ], }); + +/// IMAGINE THIS IS IN A DIFFERENT PLUGIN + +// inside the @backstage/plugin-github-pull-requests plugin +import { + createEntityCardExtension, + createEntityContentExtension, +} from '@backstage/plugin-catalog-react'; + +const githubPullRequestsPlugin = createPlugin({ + id: 'github-pull-requests', + extensions: [ + createEntityCardExtension({ + id: 'github-pull-requests', + loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), + entityFilter: isPullRequestsAvailable, + }), + createEntityContentExtension({ + id: 'github-pull-requests', + defaultPath: 'github-pull-requests', + defaultTitle: 'GitHub Pull Requests', + loader: () => + import('./PullRequestsContent').then(m => m.PullRequestsContent), + entityFilter: isPullRequestsAvailable, + }), + ], +}); + +// /deployments +const deploymentsPlugin = createPlugin({ + id: 'github-pull-requests', + extensions: [ + createEntityCardExtension({ + id: 'github-pull-requests', + attachTo: { id: 'plugin.deployments.content', input: 'cards' }, + loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), + entityFilter: isPullRequestsAvailable, + }), + createEntityContentExtension({ + id: 'github-pull-requests', + defaultPath: 'github-pull-requests', + defaultTitle: 'GitHub Pull Requests', + loader: () => + import('./PullRequestsContent').then(m => m.PullRequestsContent), + entityFilter: isPullRequestsAvailable, + }), + ], +}); From b34e8bf3a314537ac3fa16869072e4add05e3db5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 16:23:06 +0200 Subject: [PATCH 104/141] app-next: initial entity page implementation with cards and content Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- packages/app-next/src/App.tsx | 2 + .../app-next/src/examples/entityPages.tsx | 290 ++++++++++++++++++ plugins/catalog/src/alpha.tsx | 66 +--- 3 files changed, 293 insertions(+), 65 deletions(-) create mode 100644 packages/app-next/src/examples/entityPages.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 4e42d9b268..7ff47a3e89 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; +import { entityPagePlugins } from './examples/entityPages'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; import techRadarPlugin from '@backstage/plugin-tech-radar/alpha'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; @@ -120,6 +121,7 @@ const app = createApp({ techdocsPlugin, userSettingsPlugin, homePlugin, + ...entityPagePlugins, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ diff --git a/packages/app-next/src/examples/entityPages.tsx b/packages/app-next/src/examples/entityPages.tsx new file mode 100644 index 0000000000..3b5b4cceb7 --- /dev/null +++ b/packages/app-next/src/examples/entityPages.tsx @@ -0,0 +1,290 @@ +/* + * Copyright 2023 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, { useEffect } from 'react'; +import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +import { + AnyExtensionInputMap, + Extension, + ExtensionBoundary, + ExtensionInputValues, + PortableSchema, + RouteRef, + coreExtensionData, + createExtension, + createExtensionDataRef, + createExtensionInput, + createPageExtension, + createPlugin, + createSchemaFromZod, +} from '@backstage/frontend-plugin-api'; +import { + AsyncEntityProvider, + EntityLoadingStatus, + catalogApiRef, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { Expand } from '../../../frontend-plugin-api/src/types'; +import { EntityAboutCard, EntityLayout } from '@backstage/plugin-catalog'; +import { + useApi, + errorApiRef, + useRouteRefParams, +} from '@backstage/core-plugin-api'; +import { useNavigate } from 'react-router'; +import useAsyncRetry from 'react-use/lib/useAsyncRetry'; +import Grid from '@material-ui/core/Grid'; + +export const useEntityFromUrl = (): EntityLoadingStatus => { + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); + const navigate = useNavigate(); + const errorApi = useApi(errorApiRef); + const catalogApi = useApi(catalogApiRef); + + const { + value: entity, + error, + loading, + retry: refresh, + } = useAsyncRetry( + () => catalogApi.getEntityByRef({ kind, namespace, name }), + [catalogApi, kind, namespace, name], + ); + + useEffect(() => { + if (!name) { + errorApi.post(new Error('No name provided!')); + navigate('/'); + } + }, [errorApi, navigate, error, loading, entity, name]); + + return { entity, loading, error, refresh }; +}; + +export const titleExtensionDataRef = createExtensionDataRef( + 'plugin.catalog.entity.content.title', +); + +const CatalogEntityPage = createPageExtension({ + id: 'plugin.catalog.page.entity', + defaultPath: '/catalog/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(entityRouteRef), + inputs: { + contents: createExtensionInput({ + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: titleExtensionDataRef, + }), + }, + loader: async ({ inputs }) => { + const Component = () => { + return ( + + + {inputs.contents.map(content => ( + + {content.element} + + ))} + + + ); + }; + return ; + }, +}); + +export function createEntityCardExtension< + TConfig, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; +}): Extension { + return createExtension({ + id: `entity.content.${options.id}`, + attachTo: options.attachTo ?? { + id: 'entity.content.overview', + input: 'cards', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + }, + inputs: options.inputs, + configSchema: options.configSchema, + factory({ bind, config, inputs, source }) { + const LazyComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + element: ( + + + + + + ), + }); + }, + }); +} + +export function createEntityContentExtension< + TConfig extends { path: string; title: string }, + TInputs extends AnyExtensionInputMap, +>( + options: ( + | { + defaultPath: string; + defaultTitle: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): Extension { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ + path: z.string().default(options.defaultPath), + title: z.string().default(options.defaultTitle), + }), + ) as PortableSchema); + + return createExtension({ + id: `entity.content.${options.id}`, + attachTo: options.attachTo ?? { + id: 'plugin.catalog.page.entity', + input: 'contents', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: titleExtensionDataRef, + }, + inputs: options.inputs, + configSchema, + factory({ bind, config, inputs, source }) { + const LazyComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + path: config.path, + element: ( + + + + + + ), + routeRef: options.routeRef, + title: config.title, + }); + }, + }); +} + +const entityAboutCardExtension = createEntityCardExtension({ + id: 'about', + disabled: false, + loader: async () => , + // entityFilter: isDerp, +}); + +const overviewContentExtension = createEntityContentExtension({ + id: 'overview', + defaultPath: '/', + defaultTitle: 'Overview', + disabled: false, + inputs: { + cards: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => ( + + {inputs.cards.map(card => ( + + {card.element} + + ))} + + ), +}); + +const bonusTechdocsPlugin = createPlugin({ + id: 'techdocs-entity', + extensions: [ + createEntityContentExtension({ + id: 'techdocs', + defaultPath: 'docs', + defaultTitle: 'TechDocs', + disabled: false, + loader: () => + import('@backstage/plugin-techdocs').then(m => ( + + )), + // entityFilter: isPullRequestsAvailable, + }), + ], +}); + +export const entityPagePlugins = [ + createPlugin({ + id: 'entity-pages', + extensions: [ + CatalogEntityPage, + overviewContentExtension, + entityAboutCardExtension, + ], + }), + bonusTechdocsPlugin, + // deploymentsPlugin, +]; diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 0753737740..415525ab74 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -231,22 +231,6 @@ const CatalogIndexPage = createPageExtension({ }, }); -const CatalogEntityPage = createPageExtension({ - id: 'plugin.catalog.page.entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), - loader: async () => { - const Component = () => { - return ( - -
🚧 Work In Progress
-
- ); - }; - return ; - }, -}); - const CatalogNavItem = createNavItemExtension({ id: 'catalog.nav.index', routeRef: convertLegacyRouteRef(rootRouteRef), @@ -279,55 +263,7 @@ export default createPlugin({ CatalogEntityProcessingStatusFilter, CatalogEntityNamespaceFilter, CatalogIndexPage, - CatalogEntityPage, + // CatalogEntityPage, CatalogNavItem, ], }); - -/// IMAGINE THIS IS IN A DIFFERENT PLUGIN - -// inside the @backstage/plugin-github-pull-requests plugin -import { - createEntityCardExtension, - createEntityContentExtension, -} from '@backstage/plugin-catalog-react'; - -const githubPullRequestsPlugin = createPlugin({ - id: 'github-pull-requests', - extensions: [ - createEntityCardExtension({ - id: 'github-pull-requests', - loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), - entityFilter: isPullRequestsAvailable, - }), - createEntityContentExtension({ - id: 'github-pull-requests', - defaultPath: 'github-pull-requests', - defaultTitle: 'GitHub Pull Requests', - loader: () => - import('./PullRequestsContent').then(m => m.PullRequestsContent), - entityFilter: isPullRequestsAvailable, - }), - ], -}); - -// /deployments -const deploymentsPlugin = createPlugin({ - id: 'github-pull-requests', - extensions: [ - createEntityCardExtension({ - id: 'github-pull-requests', - attachTo: { id: 'plugin.deployments.content', input: 'cards' }, - loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard), - entityFilter: isPullRequestsAvailable, - }), - createEntityContentExtension({ - id: 'github-pull-requests', - defaultPath: 'github-pull-requests', - defaultTitle: 'GitHub Pull Requests', - loader: () => - import('./PullRequestsContent').then(m => m.PullRequestsContent), - entityFilter: isPullRequestsAvailable, - }), - ], -}); From c82fc7e672d73eed1ff98a9947b7fe8a98fff1ac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 16:42:54 +0200 Subject: [PATCH 105/141] catalog: split alpha entry point into multiple modules Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- plugins/catalog/package.json | 4 +- plugins/catalog/src/alpha.tsx | 269 ------------------ .../src/alpha/builtInFilterExtensions.tsx | 121 ++++++++ .../alpha/createCatalogFilterExtension.tsx | 62 ++++ plugins/catalog/src/alpha/index.ts | 18 ++ plugins/catalog/src/alpha/plugin.tsx | 145 ++++++++++ 6 files changed, 348 insertions(+), 271 deletions(-) delete mode 100644 plugins/catalog/src/alpha.tsx create mode 100644 plugins/catalog/src/alpha/builtInFilterExtensions.tsx create mode 100644 plugins/catalog/src/alpha/createCatalogFilterExtension.tsx create mode 100644 plugins/catalog/src/alpha/index.ts create mode 100644 plugins/catalog/src/alpha/plugin.tsx diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 56ae771314..214795e73c 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -10,13 +10,13 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.ts" ], "package.json": [ "package.json" diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx deleted file mode 100644 index 415525ab74..0000000000 --- a/plugins/catalog/src/alpha.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2023 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 HomeIcon from '@material-ui/icons/Home'; -import { - createApiFactory, - discoveryApiRef, - fetchApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; -import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -import { CatalogClient } from '@backstage/catalog-client'; -import { - createSchemaFromZod, - createApiExtension, - createPageExtension, - createPlugin, - createNavItemExtension, - createExtension, - coreExtensionData, - AnyExtensionInputMap, - PortableSchema, - ExtensionBoundary, - createExtensionInput, -} from '@backstage/frontend-plugin-api'; -import { - AsyncEntityProvider, - catalogApiRef, - entityRouteRef, - starredEntitiesApiRef, -} from '@backstage/plugin-catalog-react'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; -import { DefaultStarredEntitiesApi } from './apis'; -import { - createComponentRouteRef, - createFromTemplateRouteRef, - rootRouteRef, - viewTechDocRouteRef, -} from './routes'; -import { useEntityFromUrl } from './components/CatalogEntityPage/useEntityFromUrl'; - -/** @alpha */ -export const CatalogApi = createApiExtension({ - factory: createApiFactory({ - api: catalogApiRef, - deps: { - discoveryApi: discoveryApiRef, - fetchApi: fetchApiRef, - }, - factory: ({ discoveryApi, fetchApi }) => - new CatalogClient({ discoveryApi, fetchApi }), - }), -}); - -/** @alpha */ -export const StarredEntitiesApi = createApiExtension({ - factory: createApiFactory({ - api: starredEntitiesApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), - }), -}); - -/** @alpha */ -export const CatalogSearchResultListItemExtension = - createSearchResultListItemExtension({ - id: 'catalog', - predicate: result => result.type === 'software-catalog', - component: () => - import('./components/CatalogSearchResultListItem').then( - m => m.CatalogSearchResultListItem, - ), - }); - -/** @alpha */ -export function createCatalogFilterExtension< - TInputs extends AnyExtensionInputMap, - TConfig = never, ->(options: { - id: string; - inputs?: TInputs; - configSchema?: PortableSchema; - loader: (options: { config: TConfig }) => Promise; -}) { - const id = `catalog.filter.${options.id}`; - - return createExtension({ - id, - attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, - inputs: options.inputs ?? {}, - configSchema: options.configSchema, - output: { - element: coreExtensionData.reactElement, - }, - factory({ bind, config, source }) { - const ExtensionComponent = React.lazy(() => - options - .loader({ config }) - .then(element => ({ default: () => element })), - ); - - bind({ - element: ( - - - - ), - }); - }, - }); -} - -const CatalogEntityTagFilter = createCatalogFilterExtension({ - id: 'entity.tag', - loader: async () => { - const { EntityTagPicker } = await import('@backstage/plugin-catalog-react'); - return ; - }, -}); - -const CatalogEntityKindFilter = createCatalogFilterExtension({ - id: 'entity.kind', - configSchema: createSchemaFromZod(z => - z.object({ - initialFilter: z.string().default('component'), - }), - ), - loader: async ({ config }) => { - const { EntityKindPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityTypeFilter = createCatalogFilterExtension({ - id: 'entity.type', - loader: async () => { - const { EntityTypePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityOwnerFilter = createCatalogFilterExtension({ - id: 'entity.mode', - configSchema: createSchemaFromZod(z => - z.object({ - mode: z.enum(['owners-only', 'all']).optional(), - }), - ), - loader: async ({ config }) => { - const { EntityOwnerPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityNamespaceFilter = createCatalogFilterExtension({ - id: 'entity.namespace', - loader: async () => { - const { EntityNamespacePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityLifecycleFilter = createCatalogFilterExtension({ - id: 'entity.lifecycle', - loader: async () => { - const { EntityLifecyclePicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({ - id: 'entity.processing.status', - loader: async () => { - const { EntityProcessingStatusPicker } = await import( - '@backstage/plugin-catalog-react' - ); - return ; - }, -}); - -const CatalogUserListFilter = createCatalogFilterExtension({ - id: 'user.list', - configSchema: createSchemaFromZod(z => - z.object({ - initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'), - }), - ), - loader: async ({ config }) => { - const { UserListPicker } = await import('@backstage/plugin-catalog-react'); - return ; - }, -}); - -const CatalogIndexPage = createPageExtension({ - id: 'plugin.catalog.page.index', - defaultPath: '/catalog', - routeRef: convertLegacyRouteRef(rootRouteRef), - inputs: { - filters: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - loader: async ({ inputs }) => { - const { BaseCatalogPage } = await import('./components/CatalogPage'); - const filters = inputs.filters.map(filter => filter.element); - return {filters}} />; - }, -}); - -const CatalogNavItem = createNavItemExtension({ - id: 'catalog.nav.index', - routeRef: convertLegacyRouteRef(rootRouteRef), - title: 'Catalog', - icon: HomeIcon, -}); - -/** @alpha */ -export default createPlugin({ - id: 'catalog', - routes: { - catalogIndex: convertLegacyRouteRef(rootRouteRef), - catalogEntity: convertLegacyRouteRef(entityRouteRef), - }, - externalRoutes: { - viewTechDoc: convertLegacyRouteRef(viewTechDocRouteRef), - createComponent: convertLegacyRouteRef(createComponentRouteRef), - createFromTemplate: convertLegacyRouteRef(createFromTemplateRouteRef), - }, - extensions: [ - CatalogApi, - StarredEntitiesApi, - CatalogSearchResultListItemExtension, - CatalogEntityKindFilter, - CatalogEntityTypeFilter, - CatalogUserListFilter, - CatalogEntityOwnerFilter, - CatalogEntityLifecycleFilter, - CatalogEntityTagFilter, - CatalogEntityProcessingStatusFilter, - CatalogEntityNamespaceFilter, - CatalogIndexPage, - // CatalogEntityPage, - CatalogNavItem, - ], -}); diff --git a/plugins/catalog/src/alpha/builtInFilterExtensions.tsx b/plugins/catalog/src/alpha/builtInFilterExtensions.tsx new file mode 100644 index 0000000000..5a7575c4f5 --- /dev/null +++ b/plugins/catalog/src/alpha/builtInFilterExtensions.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2023 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 { createCatalogFilterExtension } from './createCatalogFilterExtension'; +import { createSchemaFromZod } from '@backstage/frontend-plugin-api'; + +const CatalogEntityTagFilter = createCatalogFilterExtension({ + id: 'entity.tag', + loader: async () => { + const { EntityTagPicker } = await import('@backstage/plugin-catalog-react'); + return ; + }, +}); + +const CatalogEntityKindFilter = createCatalogFilterExtension({ + id: 'entity.kind', + configSchema: createSchemaFromZod(z => + z.object({ + initialFilter: z.string().default('component'), + }), + ), + loader: async ({ config }) => { + const { EntityKindPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityTypeFilter = createCatalogFilterExtension({ + id: 'entity.type', + loader: async () => { + const { EntityTypePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityOwnerFilter = createCatalogFilterExtension({ + id: 'entity.mode', + configSchema: createSchemaFromZod(z => + z.object({ + mode: z.enum(['owners-only', 'all']).optional(), + }), + ), + loader: async ({ config }) => { + const { EntityOwnerPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityNamespaceFilter = createCatalogFilterExtension({ + id: 'entity.namespace', + loader: async () => { + const { EntityNamespacePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityLifecycleFilter = createCatalogFilterExtension({ + id: 'entity.lifecycle', + loader: async () => { + const { EntityLifecyclePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({ + id: 'entity.processing.status', + loader: async () => { + const { EntityProcessingStatusPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogUserListFilter = createCatalogFilterExtension({ + id: 'user.list', + configSchema: createSchemaFromZod(z => + z.object({ + initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'), + }), + ), + loader: async ({ config }) => { + const { UserListPicker } = await import('@backstage/plugin-catalog-react'); + return ; + }, +}); + +export const builtInFilterExtensions = [ + CatalogEntityTagFilter, + CatalogEntityKindFilter, + CatalogEntityTypeFilter, + CatalogEntityOwnerFilter, + CatalogEntityNamespaceFilter, + CatalogEntityLifecycleFilter, + CatalogEntityProcessingStatusFilter, + CatalogUserListFilter, +]; diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx new file mode 100644 index 0000000000..5a6328078c --- /dev/null +++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2023 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, { lazy } from 'react'; +import { + AnyExtensionInputMap, + ExtensionBoundary, + PortableSchema, + coreExtensionData, + createExtension, +} from '@backstage/frontend-plugin-api'; + +/** @alpha */ +export function createCatalogFilterExtension< + TInputs extends AnyExtensionInputMap, + TConfig = never, +>(options: { + id: string; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { config: TConfig }) => Promise; +}) { + const id = `catalog.filter.${options.id}`; + + return createExtension({ + id, + attachTo: { id: 'plugin.catalog.page.index', input: 'filters' }, + inputs: options.inputs ?? {}, + configSchema: options.configSchema, + output: { + element: coreExtensionData.reactElement, + }, + factory({ bind, config, source }) { + const ExtensionComponent = lazy(() => + options + .loader({ config }) + .then(element => ({ default: () => element })), + ); + + bind({ + element: ( + + + + ), + }); + }, + }); +} diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts new file mode 100644 index 0000000000..06a78ec6ea --- /dev/null +++ b/plugins/catalog/src/alpha/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 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 { default } from './plugin'; +export { createCatalogFilterExtension } from './createCatalogFilterExtension'; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx new file mode 100644 index 0000000000..1716b85a81 --- /dev/null +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2023 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 HomeIcon from '@material-ui/icons/Home'; +import { + createApiFactory, + discoveryApiRef, + fetchApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; +import { CatalogClient } from '@backstage/catalog-client'; +import { + createApiExtension, + createPageExtension, + createPlugin, + createNavItemExtension, + coreExtensionData, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { + AsyncEntityProvider, + catalogApiRef, + entityRouteRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; +import { DefaultStarredEntitiesApi } from '../apis'; +import { + createComponentRouteRef, + createFromTemplateRouteRef, + rootRouteRef, + viewTechDocRouteRef, +} from '../routes'; +import { builtInFilterExtensions } from './builtInFilterExtensions'; +import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; + +/** @alpha */ +export const CatalogApi = createApiExtension({ + factory: createApiFactory({ + api: catalogApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new CatalogClient({ discoveryApi, fetchApi }), + }), +}); + +/** @alpha */ +export const StarredEntitiesApi = createApiExtension({ + factory: createApiFactory({ + api: starredEntitiesApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + }), +}); + +/** @alpha */ +export const CatalogSearchResultListItemExtension = + createSearchResultListItemExtension({ + id: 'catalog', + predicate: result => result.type === 'software-catalog', + component: () => + import('../components/CatalogSearchResultListItem').then( + m => m.CatalogSearchResultListItem, + ), + }); + +const CatalogIndexPage = createPageExtension({ + id: 'plugin.catalog.page.index', + defaultPath: '/catalog', + routeRef: convertLegacyRouteRef(rootRouteRef), + inputs: { + filters: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => { + const { BaseCatalogPage } = await import('../components/CatalogPage'); + const filters = inputs.filters.map(filter => filter.element); + return {filters}} />; + }, +}); + +const CatalogEntityPage = createPageExtension({ + id: 'plugin.catalog.page.entity', + defaultPath: '/catalog/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(entityRouteRef), + loader: async () => { + const Component = () => { + return ( + +
🚧 Work In Progress
+
+ ); + }; + return ; + }, +}); + +const CatalogNavItem = createNavItemExtension({ + id: 'catalog.nav.index', + routeRef: convertLegacyRouteRef(rootRouteRef), + title: 'Catalog', + icon: HomeIcon, +}); + +/** @alpha */ +export default createPlugin({ + id: 'catalog', + routes: { + catalogIndex: convertLegacyRouteRef(rootRouteRef), + catalogEntity: convertLegacyRouteRef(entityRouteRef), + }, + externalRoutes: { + viewTechDoc: convertLegacyRouteRef(viewTechDocRouteRef), + createComponent: convertLegacyRouteRef(createComponentRouteRef), + createFromTemplate: convertLegacyRouteRef(createFromTemplateRouteRef), + }, + extensions: [ + CatalogApi, + StarredEntitiesApi, + CatalogSearchResultListItemExtension, + CatalogIndexPage, + CatalogEntityPage, + CatalogNavItem, + ...builtInFilterExtensions, + ], +}); From 0bf6ebda889bfe17f6c444ba720883f918c28a6e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 19 Oct 2023 17:04:30 +0200 Subject: [PATCH 106/141] catalog,catalog-react,techdocs: extract experimental entity page implementation from app-next Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .changeset/mighty-crews-attack.md | 5 + .changeset/nice-apes-kneel.md | 5 + .changeset/old-apricots-taste.md | 5 + packages/app-next/app-config.yaml | 9 +- packages/app-next/src/App.tsx | 19 +- .../app-next/src/examples/entityPages.tsx | 290 ------------------ plugins/catalog-react/alpha-api-report.md | 65 ++++ plugins/catalog-react/package.json | 5 +- plugins/catalog-react/src/alpha.tsx | 158 ++++++++++ plugins/catalog/alpha-api-report.md | 11 - plugins/catalog/package.json | 4 +- .../{catalog-react => catalog}/src/alpha.ts | 4 +- plugins/catalog/src/alpha/plugin.tsx | 60 +++- plugins/techdocs/src/alpha.tsx | 14 + yarn.lock | 1 + 15 files changed, 326 insertions(+), 329 deletions(-) create mode 100644 .changeset/mighty-crews-attack.md create mode 100644 .changeset/nice-apes-kneel.md create mode 100644 .changeset/old-apricots-taste.md delete mode 100644 packages/app-next/src/examples/entityPages.tsx create mode 100644 plugins/catalog-react/src/alpha.tsx rename plugins/{catalog-react => catalog}/src/alpha.ts (85%) diff --git a/.changeset/mighty-crews-attack.md b/.changeset/mighty-crews-attack.md new file mode 100644 index 0000000000..c0b8587fe0 --- /dev/null +++ b/.changeset/mighty-crews-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Initial entity page implementation for new frontend system at `/alpha`, with an overview page enabled by default and the about card available as an optional card. diff --git a/.changeset/nice-apes-kneel.md b/.changeset/nice-apes-kneel.md new file mode 100644 index 0000000000..1ecff9d223 --- /dev/null +++ b/.changeset/nice-apes-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Added entity page content for the new plugin exported via `/alpha`. diff --git a/.changeset/old-apricots-taste.md b/.changeset/old-apricots-taste.md new file mode 100644 index 0000000000..544ba11536 --- /dev/null +++ b/.changeset/old-apricots-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Added new APIs at the `/alpha` subpath for creating entity page cards and content for the new frontend system. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 6f6081fece..0c75e1c96c 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -4,12 +4,17 @@ app: routes: bindings: plugin.pages.externalRoutes.pageX: plugin.pages.routes.pageX - # waiting for https://github.com/backstage/backstage/pull/20605 - # catalog.externalRoutes.viewTechDoc: techdocs.routes.docRoot + plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot extensions: - apis.plugin.graphiql.browse.gitlab: true + # Entity page cards + - 'entity.cards.about' + + # Entity page content + - 'entity.content.techdocs' + # scmAuthExtension: >- # createScmAuthExtension({ # id: 'apis.scmAuth.addons.ghe', diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 7ff47a3e89..1275722da4 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; -import { entityPagePlugins } from './examples/entityPages'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; import techRadarPlugin from '@backstage/plugin-tech-radar/alpha'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; @@ -30,11 +29,8 @@ import { createExtension, createApiExtension, createExtensionOverrides, - createPageExtension, } from '@backstage/frontend-plugin-api'; -import { entityRouteRef } from '@backstage/plugin-catalog-react'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; -import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; import { homePage } from './HomePage'; import { collectLegacyRoutes } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; @@ -76,13 +72,6 @@ TODO: /* app.tsx */ -const entityPageExtension = createPageExtension({ - id: 'catalog:entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), - loader: async () =>
Just a temporary mocked entity page
, -}); - const homePageExtension = createExtension({ id: 'myhomepage', attachTo: { id: 'home', input: 'props' }, @@ -121,15 +110,9 @@ const app = createApp({ techdocsPlugin, userSettingsPlugin, homePlugin, - ...entityPagePlugins, ...collectedLegacyPlugins, createExtensionOverrides({ - extensions: [ - entityPageExtension, - homePageExtension, - scmAuthExtension, - scmIntegrationApi, - ], + extensions: [homePageExtension, scmAuthExtension, scmIntegrationApi], }), ], /* Handled through config instead */ diff --git a/packages/app-next/src/examples/entityPages.tsx b/packages/app-next/src/examples/entityPages.tsx deleted file mode 100644 index 3b5b4cceb7..0000000000 --- a/packages/app-next/src/examples/entityPages.tsx +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright 2023 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, { useEffect } from 'react'; -import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha'; -import { - AnyExtensionInputMap, - Extension, - ExtensionBoundary, - ExtensionInputValues, - PortableSchema, - RouteRef, - coreExtensionData, - createExtension, - createExtensionDataRef, - createExtensionInput, - createPageExtension, - createPlugin, - createSchemaFromZod, -} from '@backstage/frontend-plugin-api'; -import { - AsyncEntityProvider, - EntityLoadingStatus, - catalogApiRef, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { Expand } from '../../../frontend-plugin-api/src/types'; -import { EntityAboutCard, EntityLayout } from '@backstage/plugin-catalog'; -import { - useApi, - errorApiRef, - useRouteRefParams, -} from '@backstage/core-plugin-api'; -import { useNavigate } from 'react-router'; -import useAsyncRetry from 'react-use/lib/useAsyncRetry'; -import Grid from '@material-ui/core/Grid'; - -export const useEntityFromUrl = (): EntityLoadingStatus => { - const { kind, namespace, name } = useRouteRefParams(entityRouteRef); - const navigate = useNavigate(); - const errorApi = useApi(errorApiRef); - const catalogApi = useApi(catalogApiRef); - - const { - value: entity, - error, - loading, - retry: refresh, - } = useAsyncRetry( - () => catalogApi.getEntityByRef({ kind, namespace, name }), - [catalogApi, kind, namespace, name], - ); - - useEffect(() => { - if (!name) { - errorApi.post(new Error('No name provided!')); - navigate('/'); - } - }, [errorApi, navigate, error, loading, entity, name]); - - return { entity, loading, error, refresh }; -}; - -export const titleExtensionDataRef = createExtensionDataRef( - 'plugin.catalog.entity.content.title', -); - -const CatalogEntityPage = createPageExtension({ - id: 'plugin.catalog.page.entity', - defaultPath: '/catalog/:namespace/:kind/:name', - routeRef: convertLegacyRouteRef(entityRouteRef), - inputs: { - contents: createExtensionInput({ - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef.optional(), - title: titleExtensionDataRef, - }), - }, - loader: async ({ inputs }) => { - const Component = () => { - return ( - - - {inputs.contents.map(content => ( - - {content.element} - - ))} - - - ); - }; - return ; - }, -}); - -export function createEntityCardExtension< - TConfig, - TInputs extends AnyExtensionInputMap, ->(options: { - id: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - configSchema?: PortableSchema; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; -}): Extension { - return createExtension({ - id: `entity.content.${options.id}`, - attachTo: options.attachTo ?? { - id: 'entity.content.overview', - input: 'cards', - }, - disabled: options.disabled ?? true, - output: { - element: coreExtensionData.reactElement, - }, - inputs: options.inputs, - configSchema: options.configSchema, - factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => - options - .loader({ config, inputs }) - .then(element => ({ default: () => element })), - ); - - bind({ - element: ( - - - - - - ), - }); - }, - }); -} - -export function createEntityContentExtension< - TConfig extends { path: string; title: string }, - TInputs extends AnyExtensionInputMap, ->( - options: ( - | { - defaultPath: string; - defaultTitle: string; - } - | { - configSchema: PortableSchema; - } - ) & { - id: string; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TInputs; - routeRef?: RouteRef; - loader: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; - }, -): Extension { - const configSchema = - 'configSchema' in options - ? options.configSchema - : (createSchemaFromZod(z => - z.object({ - path: z.string().default(options.defaultPath), - title: z.string().default(options.defaultTitle), - }), - ) as PortableSchema); - - return createExtension({ - id: `entity.content.${options.id}`, - attachTo: options.attachTo ?? { - id: 'plugin.catalog.page.entity', - input: 'contents', - }, - disabled: options.disabled ?? true, - output: { - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath, - routeRef: coreExtensionData.routeRef.optional(), - title: titleExtensionDataRef, - }, - inputs: options.inputs, - configSchema, - factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => - options - .loader({ config, inputs }) - .then(element => ({ default: () => element })), - ); - - bind({ - path: config.path, - element: ( - - - - - - ), - routeRef: options.routeRef, - title: config.title, - }); - }, - }); -} - -const entityAboutCardExtension = createEntityCardExtension({ - id: 'about', - disabled: false, - loader: async () => , - // entityFilter: isDerp, -}); - -const overviewContentExtension = createEntityContentExtension({ - id: 'overview', - defaultPath: '/', - defaultTitle: 'Overview', - disabled: false, - inputs: { - cards: createExtensionInput({ - element: coreExtensionData.reactElement, - }), - }, - loader: async ({ inputs }) => ( - - {inputs.cards.map(card => ( - - {card.element} - - ))} - - ), -}); - -const bonusTechdocsPlugin = createPlugin({ - id: 'techdocs-entity', - extensions: [ - createEntityContentExtension({ - id: 'techdocs', - defaultPath: 'docs', - defaultTitle: 'TechDocs', - disabled: false, - loader: () => - import('@backstage/plugin-techdocs').then(m => ( - - )), - // entityFilter: isPullRequestsAvailable, - }), - ], -}); - -export const entityPagePlugins = [ - createPlugin({ - id: 'entity-pages', - extensions: [ - CatalogEntityPage, - overviewContentExtension, - entityAboutCardExtension, - ], - }), - bonusTechdocsPlugin, - // deploymentsPlugin, -]; diff --git a/plugins/catalog-react/alpha-api-report.md b/plugins/catalog-react/alpha-api-report.md index f9b740c07c..120e162c84 100644 --- a/plugins/catalog-react/alpha-api-report.md +++ b/plugins/catalog-react/alpha-api-report.md @@ -3,8 +3,73 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + +import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { Extension } from '@backstage/frontend-plugin-api'; +import { ExtensionInputValues } from '@backstage/frontend-plugin-api'; +import { PortableSchema } from '@backstage/frontend-plugin-api'; import { ResourcePermission } from '@backstage/plugin-permission-common'; +import { RouteRef } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +export function createEntityCardExtension< + TConfig, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; +}): Extension; + +// @alpha (undocumented) +export function createEntityContentExtension< + TConfig extends { + path: string; + title: string; + }, + TInputs extends AnyExtensionInputMap, +>( + options: ( + | { + defaultPath: string; + defaultTitle: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): Extension; + +// @alpha (undocumented) +export const entityContentTitleExtensionDataRef: ConfigurableExtensionDataRef< + string, + {} +>; // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index bb18160595..70fa5e9614 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -10,13 +10,13 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.ts", + "./alpha": "./src/alpha.tsx", "./package.json": "./package.json" }, "typesVersions": { "*": { "alpha": [ - "src/alpha.ts" + "src/alpha.tsx" ], "package.json": [ "package.json" @@ -51,6 +51,7 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx new file mode 100644 index 0000000000..d2eeb8f5f3 --- /dev/null +++ b/plugins/catalog-react/src/alpha.tsx @@ -0,0 +1,158 @@ +/* + * Copyright 2023 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 { + AnyExtensionInputMap, + Extension, + ExtensionBoundary, + ExtensionInputValues, + PortableSchema, + RouteRef, + coreExtensionData, + createExtension, + createExtensionDataRef, + createSchemaFromZod, +} from '@backstage/frontend-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { Expand } from '../../../packages/frontend-plugin-api/src/types'; + +export { isOwnerOf } from './utils'; +export { useEntityPermission } from './hooks/useEntityPermission'; + +/** @alpha */ +export const entityContentTitleExtensionDataRef = + createExtensionDataRef('plugin.catalog.entity.content.title'); + +/** @alpha */ +export function createEntityCardExtension< + TConfig, + TInputs extends AnyExtensionInputMap, +>(options: { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; +}): Extension { + const id = `entity.cards.${options.id}`; + + return createExtension({ + id, + attachTo: options.attachTo ?? { + id: 'entity.content.overview', + input: 'cards', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + }, + inputs: options.inputs, + configSchema: options.configSchema, + factory({ bind, config, inputs, source }) { + const ExtensionComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + element: ( + + + + ), + }); + }, + }); +} + +/** @alpha */ +export function createEntityContentExtension< + TConfig extends { path: string; title: string }, + TInputs extends AnyExtensionInputMap, +>( + options: ( + | { + defaultPath: string; + defaultTitle: string; + } + | { + configSchema: PortableSchema; + } + ) & { + id: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): Extension { + const id = `entity.content.${options.id}`; + + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ + path: z.string().default(options.defaultPath), + title: z.string().default(options.defaultTitle), + }), + ) as PortableSchema); + + return createExtension({ + id, + attachTo: options.attachTo ?? { + id: 'plugin.catalog.page.entity', + input: 'contents', + }, + disabled: options.disabled ?? true, + output: { + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: entityContentTitleExtensionDataRef, + }, + inputs: options.inputs, + configSchema, + factory({ bind, config, inputs, source }) { + const LazyComponent = React.lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + bind({ + path: config.path, + element: ( + + + + ), + routeRef: options.routeRef, + title: config.title, + }); + }, + }); +} diff --git a/plugins/catalog/alpha-api-report.md b/plugins/catalog/alpha-api-report.md index aba33fc092..43e589bdc3 100644 --- a/plugins/catalog/alpha-api-report.md +++ b/plugins/catalog/alpha-api-report.md @@ -12,14 +12,6 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { PortableSchema } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) -export const CatalogApi: Extension<{}>; - -// @alpha (undocumented) -export const CatalogSearchResultListItemExtension: Extension<{ - noTrack?: boolean | undefined; -}>; - // @alpha (undocumented) export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, @@ -62,8 +54,5 @@ const _default: BackstagePlugin< >; export default _default; -// @alpha (undocumented) -export const StarredEntitiesApi: Extension<{}>; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 214795e73c..9b879e3c50 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -10,13 +10,13 @@ }, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha/index.ts", + "./alpha": "./src/alpha.ts", "./package.json": "./package.json" }, "typesVersions": { "*": { "alpha": [ - "src/alpha/index.ts" + "src/alpha.ts" ], "package.json": [ "package.json" diff --git a/plugins/catalog-react/src/alpha.ts b/plugins/catalog/src/alpha.ts similarity index 85% rename from plugins/catalog-react/src/alpha.ts rename to plugins/catalog/src/alpha.ts index e8ff21609e..e80f131817 100644 --- a/plugins/catalog-react/src/alpha.ts +++ b/plugins/catalog/src/alpha.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { isOwnerOf } from './utils'; -export { useEntityPermission } from './hooks/useEntityPermission'; +export * from './alpha/index'; +export { default } from './alpha/index'; diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 1716b85a81..d9d2878213 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -38,6 +38,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; +import { + createEntityContentExtension, + createEntityCardExtension, + entityContentTitleExtensionDataRef, +} from '@backstage/plugin-catalog-react/alpha'; import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha'; import { DefaultStarredEntitiesApi } from '../apis'; import { @@ -48,6 +53,7 @@ import { } from '../routes'; import { builtInFilterExtensions } from './builtInFilterExtensions'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; +import Grid from '@material-ui/core/Grid'; /** @alpha */ export const CatalogApi = createApiExtension({ @@ -102,11 +108,30 @@ const CatalogEntityPage = createPageExtension({ id: 'plugin.catalog.page.entity', defaultPath: '/catalog/:namespace/:kind/:name', routeRef: convertLegacyRouteRef(entityRouteRef), - loader: async () => { + inputs: { + contents: createExtensionInput({ + element: coreExtensionData.reactElement, + path: coreExtensionData.routePath, + routeRef: coreExtensionData.routeRef.optional(), + title: entityContentTitleExtensionDataRef, + }), + }, + loader: async ({ inputs }) => { + const { EntityLayout } = await import('../components/EntityLayout'); const Component = () => { return ( -
🚧 Work In Progress
+ + {inputs.contents.map(content => ( + + {content.element} + + ))} +
); }; @@ -114,6 +139,35 @@ const CatalogEntityPage = createPageExtension({ }, }); +const EntityAboutCard = createEntityCardExtension({ + id: 'about', + loader: async () => + import('../components/AboutCard').then(m => ( + + )), +}); + +const OverviewEntityContent = createEntityContentExtension({ + id: 'overview', + defaultPath: '/', + defaultTitle: 'Overview', + disabled: false, + inputs: { + cards: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + loader: async ({ inputs }) => ( + + {inputs.cards.map(card => ( + + {card.element} + + ))} + + ), +}); + const CatalogNavItem = createNavItemExtension({ id: 'catalog.nav.index', routeRef: convertLegacyRouteRef(rootRouteRef), @@ -140,6 +194,8 @@ export default createPlugin({ CatalogIndexPage, CatalogEntityPage, CatalogNavItem, + OverviewEntityContent, + EntityAboutCard, ...builtInFilterExtensions, ], }); diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx index b7ccb0b12c..ceb5f47364 100644 --- a/plugins/techdocs/src/alpha.tsx +++ b/plugins/techdocs/src/alpha.tsx @@ -42,6 +42,7 @@ import { rootDocsRouteRef, rootRouteRef, } from './routes'; +import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; /** @alpha */ const techDocsStorage = createApiExtension({ @@ -141,6 +142,18 @@ const TechDocsReaderPage = createPageExtension({ )), }); +/** + * Component responsible for rendering techdocs on entity pages + * + * @alpha + */ +const TechDocsEntityContent = createEntityContentExtension({ + id: 'techdocs', + defaultPath: 'docs', + defaultTitle: 'TechDocs', + loader: () => import('./Router').then(m => ), +}); + /** @alpha */ const TechDocsNavItem = createNavItemExtension({ id: 'plugin.techdocs.nav.index', @@ -158,6 +171,7 @@ export default createPlugin({ TechDocsNavItem, TechDocsIndexPage, TechDocsReaderPage, + TechDocsEntityContent, TechDocsSearchResultListItemExtension, ], routes: { diff --git a/yarn.lock b/yarn.lock index 059c18d859..3075745d85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5837,6 +5837,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" From a9611d42518156fc568ddf023713e345be99720f Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 20 Oct 2023 09:52:47 +0200 Subject: [PATCH 107/141] fix(catalog-react): make entity content routable Signed-off-by: Camila Belo --- plugins/catalog-react/src/alpha.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index d2eeb8f5f3..58762b1994 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { lazy } from 'react'; import { AnyExtensionInputMap, Extension, @@ -67,7 +67,7 @@ export function createEntityCardExtension< inputs: options.inputs, configSchema: options.configSchema, factory({ bind, config, inputs, source }) { - const ExtensionComponent = React.lazy(() => + const ExtensionComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), @@ -137,7 +137,7 @@ export function createEntityContentExtension< inputs: options.inputs, configSchema, factory({ bind, config, inputs, source }) { - const LazyComponent = React.lazy(() => + const ExtensionComponent = lazy(() => options .loader({ config, inputs }) .then(element => ({ default: () => element })), @@ -145,13 +145,13 @@ export function createEntityContentExtension< bind({ path: config.path, + title: config.title, + routeRef: options.routeRef, element: ( - - + + ), - routeRef: options.routeRef, - title: config.title, }); }, }); From 6bf7561d3c264d8c3bc03ca8443d72e6ff82ce81 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 20 Oct 2023 11:42:19 +0200 Subject: [PATCH 108/141] cli: fix package detection breaking if package.json is not available Signed-off-by: Patrik Oldsberg --- .changeset/rich-pugs-chew.md | 5 +++ .../cli/src/lib/bundler/packageDetection.ts | 38 ++++++++++--------- 2 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 .changeset/rich-pugs-chew.md diff --git a/.changeset/rich-pugs-chew.md b/.changeset/rich-pugs-chew.md new file mode 100644 index 0000000000..6c5aa0af62 --- /dev/null +++ b/.changeset/rich-pugs-chew.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The experimental package detection will now ignore packages that don't make `package.json` available. diff --git a/packages/cli/src/lib/bundler/packageDetection.ts b/packages/cli/src/lib/bundler/packageDetection.ts index 2095fe23e7..3aa0955c06 100644 --- a/packages/cli/src/lib/bundler/packageDetection.ts +++ b/packages/cli/src/lib/bundler/packageDetection.ts @@ -77,24 +77,28 @@ async function detectPackages( return []; } - const depPackageJson: BackstagePackageJson = require(require.resolve( - `${depName}/package.json`, - { paths: [targetPath] }, - )); - if ( - ['frontend-plugin', 'frontend-plugin-module'].includes( - depPackageJson.backstage?.role ?? '', - ) - ) { - // Include alpha entry point if available. If there's no default export it will be ignored - const exp = depPackageJson.exports; - if (exp && typeof exp === 'object' && './alpha' in exp) { - return [ - { name: depName, import: depName }, - { name: depName, export: './alpha', import: `${depName}/alpha` }, - ]; + try { + const depPackageJson: BackstagePackageJson = require(require.resolve( + `${depName}/package.json`, + { paths: [targetPath] }, + )); + if ( + ['frontend-plugin', 'frontend-plugin-module'].includes( + depPackageJson.backstage?.role ?? '', + ) + ) { + // Include alpha entry point if available. If there's no default export it will be ignored + const exp = depPackageJson.exports; + if (exp && typeof exp === 'object' && './alpha' in exp) { + return [ + { name: depName, import: depName }, + { name: depName, export: './alpha', import: `${depName}/alpha` }, + ]; + } + return [{ name: depName, import: depName }]; } - return [{ name: depName, import: depName }]; + } catch { + /* ignore packages that don't make package.json available */ } return []; }); From f75caf9f3d3e9b5352686c9fe4f9fd35abc43716 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 20 Oct 2023 12:44:25 +0200 Subject: [PATCH 109/141] search-react: fix search bar race Signed-off-by: Patrik Oldsberg --- .changeset/real-carrots-brake.md | 5 ++++ .../components/SearchBar/SearchBar.test.tsx | 1 - .../src/components/SearchBar/SearchBar.tsx | 27 +++++++++++++++---- .../SearchModal/SearchModal.test.tsx | 3 +-- 4 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 .changeset/real-carrots-brake.md diff --git a/.changeset/real-carrots-brake.md b/.changeset/real-carrots-brake.md new file mode 100644 index 0000000000..e9ae1f0c27 --- /dev/null +++ b/.changeset/real-carrots-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Fixed a rare occurrence where a race in the search bar could throw away user input or cause the clear button not to work. diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index d9b29584f8..8a3dd84286 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -339,7 +339,6 @@ describe('SearchBar', () => { value = 'new value'; await user.clear(textbox); - await waitFor(() => expect(textbox.value).toBe('')); // make sure new term is captured await user.type(textbox, value); diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx index cfd0538694..d101efa669 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -30,6 +30,7 @@ import React, { KeyboardEvent, useCallback, useEffect, + useRef, useState, } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; @@ -88,14 +89,28 @@ export const SearchBarBase: ForwardRefExoticComponent = const configApi = useApi(configApiRef); const [value, setValue] = useState(''); + const forwardedValueRef = useRef(''); useEffect(() => { - setValue(prevValue => - prevValue !== defaultValue ? String(defaultValue) : prevValue, - ); - }, [defaultValue]); + setValue(prevValue => { + // We only update the value if our current value is the same as it was + // for the most recent onChange call. Otherwise it means that the users + // has continued typing and we should not replace their input. + if (prevValue === forwardedValueRef.current) { + return String(defaultValue); + } + return prevValue; + }); + }, [defaultValue, forwardedValueRef]); - useDebounce(() => onChange(value), debounceTime, [value]); + useDebounce( + () => { + forwardedValueRef.current = value; + onChange(value); + }, + debounceTime, + [value], + ); const handleChange = useCallback( (e: ChangeEvent) => { @@ -115,7 +130,9 @@ export const SearchBarBase: ForwardRefExoticComponent = ); const handleClear = useCallback(() => { + forwardedValueRef.current = ''; onChange(''); + setValue(''); if (onClear) { onClear(); } diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index 6fce560800..0d0bfdef5c 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { screen, waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; @@ -205,7 +205,6 @@ describe('SearchModal', () => { const input = screen.getByLabelText('Search'); await userEvent.clear(input); - await waitFor(() => expect(input.value).toBe('')); await userEvent.type(input, 'new term{enter}'); expect(navigate).toHaveBeenCalledWith('/search?query=new term'); From 5c5a5a8207b7cd271c13ac752463085203665715 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 18 Sep 2023 13:18:29 +0200 Subject: [PATCH 110/141] feat: work on enable vite Signed-off-by: blam --- packages/cli/package.json | 2 + packages/cli/src/lib/bundler/server.ts | 143 ++++++++++++++++--------- 2 files changed, 93 insertions(+), 52 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 419b02d929..1a1909a87c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,6 +68,7 @@ "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "6.7.5", "@typescript-eslint/parser": "^6.7.2", + "@vitejs/plugin-react": "^4.0.4", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "^3.0.0-rc.4", "bfj": "^7.0.2", @@ -132,6 +133,7 @@ "tar": "^6.1.12", "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", + "vite": "^4.4.9", "webpack": "^5.70.0", "webpack-dev-server": "^4.7.3", "webpack-node-externals": "^3.0.0", diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 78f6bb7823..7cf089607c 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -22,6 +22,8 @@ import uniq from 'lodash/uniq'; import openBrowser from 'react-dev-utils/openBrowser'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; +import vite from 'vite'; +import react from '@vitejs/plugin-react'; import { forbiddenDuplicatesFilter, @@ -77,7 +79,7 @@ export async function serveBundle(options: ServeOptions) { const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); - let server: WebpackDevServer | undefined = undefined; + let server: WebpackDevServer | vite.ViteDevServer | undefined = undefined; let latestFrontendAppConfigs: AppConfig[] = []; const cliConfig = await loadCliConfig({ @@ -86,7 +88,15 @@ export async function serveBundle(options: ServeOptions) { withFilteredKeys: true, watch(appConfigs) { latestFrontendAppConfigs = appConfigs; - server?.invalidate(); + if (server) { + if ('invalidate' in server) { + server?.invalidate(); + } + + if ('restart' in server) { + server?.restart(); + } + } }, }); latestFrontendAppConfigs = cliConfig.frontendAppConfigs; @@ -123,7 +133,15 @@ export async function serveBundle(options: ServeOptions) { config: fullConfig, targetPath: paths.targetPath, watch() { - server?.invalidate(); + if (server) { + if ('invalidate' in server) { + server?.invalidate(); + } + + if ('restart' in server) { + server?.restart(); + } + } }, }); @@ -139,58 +157,79 @@ export async function serveBundle(options: ServeOptions) { additionalEntryPoints: detectedModulesEntryPoint, }); - const compiler = webpack(config); - - server = new WebpackDevServer( - { - hot: !process.env.CI, - devMiddleware: { - publicPath: config.output?.publicPath as string, - stats: 'errors-warnings', + if (process.env.EXPERIMENTAL_VITE) { + server = await vite.createServer({ + plugins: [react()], + server: { + host, + port, }, - static: paths.targetPublic - ? { - publicPath: config.output?.publicPath as string, - directory: paths.targetPublic, - } - : undefined, - historyApiFallback: { - // Paths with dots should still use the history fallback. - // See https://github.com/facebookincubator/create-react-app/issues/387. - disableDotRule: true, - - // The index needs to be rewritten relative to the new public path, including subroutes. - index: `${config.output?.publicPath}index.html`, + build: { + commonjsOptions: { + include: ['*'], + transformMixedEsModules: true, + }, }, - https: - url.protocol === 'https:' - ? { - cert: fullConfig.getString('app.https.certificate.cert'), - key: fullConfig.getString('app.https.certificate.key'), - } - : false, - host, - port, - proxy: targetPkg.proxy, - // When the dev server is behind a proxy, the host and public hostname differ - allowedHosts: [url.hostname], - client: { - webSocketURL: 'auto://0.0.0.0:0/ws', - }, - } as any, - compiler as any, - ); - - await new Promise((resolve, reject) => { - server?.startCallback((err?: Error) => { - if (err) { - reject(err); - return; - } - - openBrowser(url.href); - resolve(); + publicDir: paths.targetPublic, }); + } else { + const compiler = webpack(config); + + server = new WebpackDevServer( + { + hot: !process.env.CI, + devMiddleware: { + publicPath: config.output?.publicPath as string, + stats: 'errors-warnings', + }, + static: paths.targetPublic + ? { + publicPath: config.output?.publicPath as string, + directory: paths.targetPublic, + } + : undefined, + historyApiFallback: { + // Paths with dots should still use the history fallback. + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, + + // The index needs to be rewritten relative to the new public path, including subroutes. + index: `${config.output?.publicPath}index.html`, + }, + https: + url.protocol === 'https:' + ? { + cert: fullConfig.getString('app.https.certificate.cert'), + key: fullConfig.getString('app.https.certificate.key'), + } + : false, + host, + port, + proxy: targetPkg.proxy, + // When the dev server is behind a proxy, the host and public hostname differ + allowedHosts: [url.hostname], + client: { + webSocketURL: 'auto://0.0.0.0:0/ws', + }, + } as any, + compiler as any, + ); + } + + await new Promise(async (resolve, reject) => { + if (process.env.EXPERIMENTAL_VITE) { + await (server as vite.ViteDevServer).listen(); + resolve(); + } else { + (server as WebpackDevServer).startCallback((err?: Error) => { + if (err) { + reject(err); + return; + } + }); + } + openBrowser(url.href); + resolve(); }); const waitForExit = async () => { From 0e6fc26cbf55e338579b6c93de18dfef0b5ffe5f Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 21 Sep 2023 13:33:41 +0200 Subject: [PATCH 111/141] chore: some more vite work Signed-off-by: blam --- packages/cli/package.json | 4 +++ packages/cli/src/lib/bundler/server.ts | 43 +++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 1a1909a87c..ae181bceef 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,11 +42,13 @@ "@backstage/types": "workspace:^", "@esbuild-kit/cjs-loader": "^2.4.1", "@esbuild-kit/esm-loader": "^2.5.5", + "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@manypkg/get-packages": "^1.1.3", "@octokit/graphql": "^5.0.0", "@octokit/graphql-schema": "^13.7.0", "@octokit/oauth-app": "^4.2.0", "@octokit/request": "^6.0.0", + "@originjs/vite-plugin-commonjs": "^1.0.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@rollup/plugin-commonjs": "^23.0.0", "@rollup/plugin-json": "^5.0.0", @@ -134,6 +136,8 @@ "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", "vite": "^4.4.9", + "vite-plugin-node-polyfills": "^0.14.1", + "vite-plugin-svgr": "^4.0.0", "webpack": "^5.70.0", "webpack-dev-server": "^4.7.3", "webpack-node-externals": "^3.0.0", diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 7cf089607c..455c18d3c2 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -36,6 +36,10 @@ import { createConfig, resolveBaseUrl } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths } from './paths'; import { ServeOptions } from './types'; +import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; +import { esbuildCommonjs, viteCommonjs } from '@originjs/vite-plugin-commonjs'; +import pluginSvgr from 'vite-plugin-svgr'; +import vitePluginSvgr from 'vite-plugin-svgr'; export async function serveBundle(options: ServeOptions) { const paths = resolveBundlingPaths(options); @@ -159,11 +163,47 @@ export async function serveBundle(options: ServeOptions) { if (process.env.EXPERIMENTAL_VITE) { server = await vite.createServer({ - plugins: [react()], + define: { + global: 'globalThis', + APP_CONFIG: JSON.stringify(cliConfig.frontendAppConfigs), + }, + resolve: { + alias: { + 'node-fetch': 'cross-fetch', + }, + }, + plugins: [ + react(), + vitePluginSvgr(), + viteCommonjs(), + viteNodePolyfills(), + { + name: 'transform-index-html', + configureServer(s) { + s.middlewares.use(async (req, res, next) => { + if (req.url === '/') { + res.end( + await s.transformIndexHtml( + req.url, + await fs.readFile(paths.targetHtml, 'utf-8'), + ), + ); + } else { + next(); + } + }); + }, + }, + ], server: { host, port, }, + optimizeDeps: { + esbuildOptions: { + plugins: [esbuildCommonjs(['!nano-css'])], + }, + }, build: { commonjsOptions: { include: ['*'], @@ -171,6 +211,7 @@ export async function serveBundle(options: ServeOptions) { }, }, publicDir: paths.targetPublic, + root: paths.targetPath, }); } else { const compiler = webpack(config); From b3989e18a1b4ab41f10a80288cc138d44c5f4f98 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 2 Oct 2023 15:32:51 +0200 Subject: [PATCH 112/141] chore: some little trickery Signed-off-by: blam --- packages/cli/package.json | 1 + packages/cli/src/lib/bundler/server.ts | 20 ++------------------ 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index ae181bceef..e2f26b8000 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -136,6 +136,7 @@ "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", "vite": "^4.4.9", + "vite-plugin-html-template": "^1.2.0", "vite-plugin-node-polyfills": "^0.14.1", "vite-plugin-svgr": "^4.0.0", "webpack": "^5.70.0", diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 455c18d3c2..48a020ecf2 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -38,7 +38,7 @@ import { resolveBundlingPaths } from './paths'; import { ServeOptions } from './types'; import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; import { esbuildCommonjs, viteCommonjs } from '@originjs/vite-plugin-commonjs'; -import pluginSvgr from 'vite-plugin-svgr'; +import htmlTemplate from 'vite-plugin-html-template'; import vitePluginSvgr from 'vite-plugin-svgr'; export async function serveBundle(options: ServeOptions) { @@ -177,23 +177,7 @@ export async function serveBundle(options: ServeOptions) { vitePluginSvgr(), viteCommonjs(), viteNodePolyfills(), - { - name: 'transform-index-html', - configureServer(s) { - s.middlewares.use(async (req, res, next) => { - if (req.url === '/') { - res.end( - await s.transformIndexHtml( - req.url, - await fs.readFile(paths.targetHtml, 'utf-8'), - ), - ); - } else { - next(); - } - }); - }, - }, + htmlTemplate(), ], server: { host, From 605c0d171bf2a5e76ad804b24d9d64de27b1fbeb Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 2 Oct 2023 15:51:56 +0200 Subject: [PATCH 113/141] chore: more small work Signed-off-by: blam --- packages/app/public/index.html | 44 +- packages/cli/src/lib/bundler/server.ts | 8 +- yarn.lock | 1084 ++++++++++++++++++++++-- 3 files changed, 1009 insertions(+), 127 deletions(-) diff --git a/packages/app/public/index.html b/packages/app/public/index.html index a3c3ef19b8..4e22ba45e8 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -41,43 +41,6 @@ href="<%= publicPath %>/safari-pinned-tab.svg" color="#5bbad5" /> - <%= config.getString('app.title') %> - - <% if (config.has('app.datadogRum')) { %> - - <% } %> @@ -93,5 +56,12 @@ To begin the development, run `yarn start`. To create a production bundle, use `yarn build`. --> + + + + <% } %> @@ -56,12 +93,5 @@ To begin the development, run `yarn start`. To create a production bundle, use `yarn build`. --> - - - `; + + if (req.url === '/') { + res.end(await s.transformIndexHtml(req.url, rendered)); + } else { + next(); + } + }); + }, +}); diff --git a/yarn.lock b/yarn.lock index 98e0a6358b..eb2088e9cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3833,6 +3833,7 @@ __metadata: "@swc/jest": ^0.2.22 "@types/cross-spawn": ^6.0.2 "@types/diff": ^5.0.0 + "@types/ejs": ^3.1.3 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.1 "@types/http-proxy": ^1.17.4 @@ -3865,6 +3866,7 @@ __metadata: ctrlc-windows: ^2.1.0 del: ^7.0.0 diff: ^5.0.0 + ejs: ^3.1.9 esbuild: ^0.19.0 esbuild-loader: ^2.18.0 eslint: ^8.6.0 @@ -3922,6 +3924,7 @@ __metadata: type-fest: ^2.19.0 util: ^0.12.3 vite: ^4.4.9 + vite-plugin-html: ^3.2.0 vite-plugin-html-template: ^1.2.0 vite-plugin-node-polyfills: ^0.14.1 vite-plugin-svgr: ^4.0.0 @@ -15035,7 +15038,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^4.1.1, @rollup/pluginutils@npm:^4.2.1": +"@rollup/pluginutils@npm:^4.1.1, @rollup/pluginutils@npm:^4.2.0, @rollup/pluginutils@npm:^4.2.1": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" dependencies: @@ -17617,6 +17620,13 @@ __metadata: languageName: node linkType: hard +"@types/ejs@npm:^3.1.3": + version: 3.1.3 + resolution: "@types/ejs@npm:3.1.3" + checksum: b1b1c6c9d331d237523ebc410789f42edcdbb1d4cdd4a7a37ac61d2ce9c3fbcfbfe7d7f1a7f61c9334812347a0036afd52258ad2198f85545ebfb26d63475a75 + languageName: node + linkType: hard + "@types/es-aggregate-error@npm:^1.0.2": version: 1.0.2 resolution: "@types/es-aggregate-error@npm:1.0.2" @@ -22617,6 +22627,13 @@ __metadata: languageName: node linkType: hard +"connect-history-api-fallback@npm:^1.6.0": + version: 1.6.0 + resolution: "connect-history-api-fallback@npm:1.6.0" + checksum: 804ca2be28c999032ecd37a9f71405e5d7b7a4b3defcebbe41077bb8c5a0a150d7b59f51dcc33b2de30bc7e217a31d10f8cfad27e8e74c2fc7655eeba82d6e7e + languageName: node + linkType: hard + "connect-history-api-fallback@npm:^2.0.0": version: 2.0.0 resolution: "connect-history-api-fallback@npm:2.0.0" @@ -22634,7 +22651,7 @@ __metadata: languageName: node linkType: hard -"consola@npm:^2.15.0": +"consola@npm:^2.15.0, consola@npm:^2.15.3": version: 2.15.3 resolution: "consola@npm:2.15.3" checksum: 8ef7a09b703ec67ac5c389a372a33b6dc97eda6c9876443a60d76a3076eea0259e7f67a4e54fd5a52f97df73690822d090cf8b7e102b5761348afef7c6d03e28 @@ -23123,16 +23140,16 @@ __metadata: languageName: node linkType: hard -"css-select@npm:^4.1.3": - version: 4.1.3 - resolution: "css-select@npm:4.1.3" +"css-select@npm:^4.1.3, css-select@npm:^4.2.1": + version: 4.3.0 + resolution: "css-select@npm:4.3.0" dependencies: boolbase: ^1.0.0 - css-what: ^5.0.0 - domhandler: ^4.2.0 - domutils: ^2.6.0 - nth-check: ^2.0.0 - checksum: 40928f1aa6c71faf36430e7f26bcbb8ab51d07b98b754caacb71906400a195df5e6c7020a94f2982f02e52027b9bd57c99419220cf7020968c3415f14e4be5f8 + css-what: ^6.0.1 + domhandler: ^4.3.1 + domutils: ^2.8.0 + nth-check: ^2.0.1 + checksum: d6202736839194dd7f910320032e7cfc40372f025e4bf21ca5bf6eb0a33264f322f50ba9c0adc35dadd342d3d6fae5ca244779a4873afbfa76561e343f2058e0 languageName: node linkType: hard @@ -23196,14 +23213,7 @@ __metadata: languageName: node linkType: hard -"css-what@npm:^5.0.0": - version: 5.1.0 - resolution: "css-what@npm:5.1.0" - checksum: 0b75d1bac95c885c168573c85744a6c6843d8c33345f54f717218b37ea6296b0e99bb12105930ea170fd4a921990392a7c790c16c585c1d8960c49e2b7ec39f7 - languageName: node - linkType: hard - -"css-what@npm:^6.1.0": +"css-what@npm:^6.0.1, css-what@npm:^6.1.0": version: 6.1.0 resolution: "css-what@npm:6.1.0" checksum: b975e547e1e90b79625918f84e67db5d33d896e6de846c9b584094e529f0c63e2ab85ee33b9daffd05bff3a146a1916bec664e18bb76dd5f66cbff9fc13b2bbe @@ -24218,12 +24228,12 @@ __metadata: languageName: node linkType: hard -"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0": - version: 4.3.0 - resolution: "domhandler@npm:4.3.0" +"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": + version: 4.3.1 + resolution: "domhandler@npm:4.3.1" dependencies: domelementtype: ^2.2.0 - checksum: d2a2dbf40dd99abf936b65ad83c6b530afdb3605a87cad37a11b5d9220e68423ebef1b86c89e0f6d93ffaf315cc327cf1a988652e7a9a95cce539e3984f4c64d + checksum: 4c665ceed016e1911bf7d1dadc09dc888090b64dee7851cccd2fcf5442747ec39c647bb1cb8c8919f8bbdd0f0c625a6bafeeed4b2d656bbecdbae893f43ffaaa languageName: node linkType: hard @@ -24250,7 +24260,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^2.5.2, domutils@npm:^2.6.0": +"domutils@npm:^2.5.2, domutils@npm:^2.8.0": version: 2.8.0 resolution: "domutils@npm:2.8.0" dependencies: @@ -24291,6 +24301,13 @@ __metadata: languageName: node linkType: hard +"dotenv-expand@npm:^8.0.2": + version: 8.0.3 + resolution: "dotenv-expand@npm:8.0.3" + checksum: 128ce90ac825b543de3ece0154a51b056ab0dc36bb26d97a68cd0b8707327ecd3c182fb6ac63b26a0fcdfa85064419906a1065cb634f1f9dc08ad311375f1fc0 + languageName: node + linkType: hard + "dotenv@npm:^16.0.0": version: 16.0.0 resolution: "dotenv@npm:16.0.0" @@ -24411,14 +24428,14 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.6": - version: 3.1.7 - resolution: "ejs@npm:3.1.7" +"ejs@npm:^3.1.6, ejs@npm:^3.1.9": + version: 3.1.9 + resolution: "ejs@npm:3.1.9" dependencies: jake: ^10.8.5 bin: ejs: bin/cli.js - checksum: fe40764af39955ce8f8b116716fc8b911959946698edb49ecab85df597746c07aa65d5b74ead28a1e2ffa75b0f92d9bedd752f1c29437da6137b3518271e988c + checksum: af6f10eb815885ff8a8cfacc42c6b6cf87daf97a4884f87a30e0c3271fedd85d76a3a297d9c33a70e735b97ee632887f85e32854b9cdd3a2d97edf931519a35f languageName: node linkType: hard @@ -28473,7 +28490,7 @@ __metadata: languageName: node linkType: hard -"html-minifier-terser@npm:^6.0.2": +"html-minifier-terser@npm:^6.0.2, html-minifier-terser@npm:^6.1.0": version: 6.1.0 resolution: "html-minifier-terser@npm:6.1.0" dependencies: @@ -34214,6 +34231,16 @@ __metadata: languageName: node linkType: hard +"node-html-parser@npm:^5.3.3": + version: 5.4.2 + resolution: "node-html-parser@npm:5.4.2" + dependencies: + css-select: ^4.2.1 + he: 1.2.0 + checksum: 2d2391147c83b402786eeab95d23ea4e24ca8608e0e70a2823bfd4f2a248be13a8cc31acfd55a0109e051131e4f0c17d7ada8d999ce70ff2e342ab0110f5da59 + languageName: node + linkType: hard + "node-html-parser@npm:^6.1.1": version: 6.1.5 resolution: "node-html-parser@npm:6.1.5" @@ -34557,7 +34584,7 @@ __metadata: languageName: node linkType: hard -"nth-check@npm:^2.0.0, nth-check@npm:^2.0.1": +"nth-check@npm:^2.0.1": version: 2.1.1 resolution: "nth-check@npm:2.1.1" dependencies: @@ -35670,6 +35697,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^0.2.0": + version: 0.2.0 + resolution: "pathe@npm:0.2.0" + checksum: 9a8149ce152088f30d15b0b03a7c128ba21f16b4dc1f3f90fe38eee9f6d0f1d6da8e4e47bd2a4f9e14aaac7c30ed01cfc86216479011de2bdc598b65e6f19f41 + languageName: node + linkType: hard + "pause@npm:0.0.1": version: 0.0.1 resolution: "pause@npm:0.0.1" @@ -42799,6 +42833,28 @@ __metadata: languageName: node linkType: hard +"vite-plugin-html@npm:^3.2.0": + version: 3.2.0 + resolution: "vite-plugin-html@npm:3.2.0" + dependencies: + "@rollup/pluginutils": ^4.2.0 + colorette: ^2.0.16 + connect-history-api-fallback: ^1.6.0 + consola: ^2.15.3 + dotenv: ^16.0.0 + dotenv-expand: ^8.0.2 + ejs: ^3.1.6 + fast-glob: ^3.2.11 + fs-extra: ^10.0.1 + html-minifier-terser: ^6.1.0 + node-html-parser: ^5.3.3 + pathe: ^0.2.0 + peerDependencies: + vite: ">=2.0.0" + checksum: f5222247b65da1c36215f0b2f509fd3975a7426b8d44546beb49f3ba51ee87b3a6b6e6afc9e7567a0d8bd1016631f2db3f934808f62a7c8f7f83fa83d8561d2d + languageName: node + linkType: hard + "vite-plugin-node-polyfills@npm:^0.14.1": version: 0.14.1 resolution: "vite-plugin-node-polyfills@npm:0.14.1" From cf3fe0510f708d19713cd940ad0bbc92eb672c6c Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 3 Oct 2023 12:16:54 +0200 Subject: [PATCH 115/141] chore: initial version of vite Signed-off-by: blam --- packages/cli/package.json | 2 - yarn.lock | 85 ++------------------------------------- 2 files changed, 4 insertions(+), 83 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index d2e0b9e8ec..86d1ae5fa9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -137,8 +137,6 @@ "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", "vite": "^4.4.9", - "vite-plugin-html": "^3.2.0", - "vite-plugin-html-template": "^1.2.0", "vite-plugin-node-polyfills": "^0.14.1", "vite-plugin-svgr": "^4.0.0", "webpack": "^5.70.0", diff --git a/yarn.lock b/yarn.lock index eb2088e9cc..118e8dba2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3924,8 +3924,6 @@ __metadata: type-fest: ^2.19.0 util: ^0.12.3 vite: ^4.4.9 - vite-plugin-html: ^3.2.0 - vite-plugin-html-template: ^1.2.0 vite-plugin-node-polyfills: ^0.14.1 vite-plugin-svgr: ^4.0.0 webpack: ^5.70.0 @@ -15038,7 +15036,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^4.1.1, @rollup/pluginutils@npm:^4.2.0, @rollup/pluginutils@npm:^4.2.1": +"@rollup/pluginutils@npm:^4.1.1, @rollup/pluginutils@npm:^4.2.1": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" dependencies: @@ -22627,13 +22625,6 @@ __metadata: languageName: node linkType: hard -"connect-history-api-fallback@npm:^1.6.0": - version: 1.6.0 - resolution: "connect-history-api-fallback@npm:1.6.0" - checksum: 804ca2be28c999032ecd37a9f71405e5d7b7a4b3defcebbe41077bb8c5a0a150d7b59f51dcc33b2de30bc7e217a31d10f8cfad27e8e74c2fc7655eeba82d6e7e - languageName: node - linkType: hard - "connect-history-api-fallback@npm:^2.0.0": version: 2.0.0 resolution: "connect-history-api-fallback@npm:2.0.0" @@ -22651,7 +22642,7 @@ __metadata: languageName: node linkType: hard -"consola@npm:^2.15.0, consola@npm:^2.15.3": +"consola@npm:^2.15.0": version: 2.15.3 resolution: "consola@npm:2.15.3" checksum: 8ef7a09b703ec67ac5c389a372a33b6dc97eda6c9876443a60d76a3076eea0259e7f67a4e54fd5a52f97df73690822d090cf8b7e102b5761348afef7c6d03e28 @@ -23140,7 +23131,7 @@ __metadata: languageName: node linkType: hard -"css-select@npm:^4.1.3, css-select@npm:^4.2.1": +"css-select@npm:^4.1.3": version: 4.3.0 resolution: "css-select@npm:4.3.0" dependencies: @@ -24301,13 +24292,6 @@ __metadata: languageName: node linkType: hard -"dotenv-expand@npm:^8.0.2": - version: 8.0.3 - resolution: "dotenv-expand@npm:8.0.3" - checksum: 128ce90ac825b543de3ece0154a51b056ab0dc36bb26d97a68cd0b8707327ecd3c182fb6ac63b26a0fcdfa85064419906a1065cb634f1f9dc08ad311375f1fc0 - languageName: node - linkType: hard - "dotenv@npm:^16.0.0": version: 16.0.0 resolution: "dotenv@npm:16.0.0" @@ -28490,7 +28474,7 @@ __metadata: languageName: node linkType: hard -"html-minifier-terser@npm:^6.0.2, html-minifier-terser@npm:^6.1.0": +"html-minifier-terser@npm:^6.0.2": version: 6.1.0 resolution: "html-minifier-terser@npm:6.1.0" dependencies: @@ -34231,16 +34215,6 @@ __metadata: languageName: node linkType: hard -"node-html-parser@npm:^5.3.3": - version: 5.4.2 - resolution: "node-html-parser@npm:5.4.2" - dependencies: - css-select: ^4.2.1 - he: 1.2.0 - checksum: 2d2391147c83b402786eeab95d23ea4e24ca8608e0e70a2823bfd4f2a248be13a8cc31acfd55a0109e051131e4f0c17d7ada8d999ce70ff2e342ab0110f5da59 - languageName: node - linkType: hard - "node-html-parser@npm:^6.1.1": version: 6.1.5 resolution: "node-html-parser@npm:6.1.5" @@ -35697,13 +35671,6 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^0.2.0": - version: 0.2.0 - resolution: "pathe@npm:0.2.0" - checksum: 9a8149ce152088f30d15b0b03a7c128ba21f16b4dc1f3f90fe38eee9f6d0f1d6da8e4e47bd2a4f9e14aaac7c30ed01cfc86216479011de2bdc598b65e6f19f41 - languageName: node - linkType: hard - "pause@npm:0.0.1": version: 0.0.1 resolution: "pause@npm:0.0.1" @@ -39610,19 +39577,6 @@ __metadata: languageName: node linkType: hard -"shelljs@npm:0.8.4": - version: 0.8.4 - resolution: "shelljs@npm:0.8.4" - dependencies: - glob: ^7.0.0 - interpret: ^1.0.0 - rechoir: ^0.6.2 - bin: - shjs: bin/shjs - checksum: 27f83206ef6a4f5b74a493726c3e6b4c3e07a9c2aac94c5e692d800a61353c18a8234967bd8523b1346abe718beb563843687fb57f466529ba06db3cae6f0bb3 - languageName: node - linkType: hard - "shelljs@npm:^0.8.5": version: 0.8.5 resolution: "shelljs@npm:0.8.5" @@ -42824,37 +42778,6 @@ __metadata: languageName: node linkType: hard -"vite-plugin-html-template@npm:^1.2.0": - version: 1.2.0 - resolution: "vite-plugin-html-template@npm:1.2.0" - dependencies: - shelljs: 0.8.4 - checksum: ca157c02b13cded3136818ee399e5a1caccb41836d4407680c16c4ad3449aab7fc6b1f62376a2cc9620dff6a5144c831a44dbdf21f85eb19b0cdc768480f2aad - languageName: node - linkType: hard - -"vite-plugin-html@npm:^3.2.0": - version: 3.2.0 - resolution: "vite-plugin-html@npm:3.2.0" - dependencies: - "@rollup/pluginutils": ^4.2.0 - colorette: ^2.0.16 - connect-history-api-fallback: ^1.6.0 - consola: ^2.15.3 - dotenv: ^16.0.0 - dotenv-expand: ^8.0.2 - ejs: ^3.1.6 - fast-glob: ^3.2.11 - fs-extra: ^10.0.1 - html-minifier-terser: ^6.1.0 - node-html-parser: ^5.3.3 - pathe: ^0.2.0 - peerDependencies: - vite: ">=2.0.0" - checksum: f5222247b65da1c36215f0b2f509fd3975a7426b8d44546beb49f3ba51ee87b3a6b6e6afc9e7567a0d8bd1016631f2db3f934808f62a7c8f7f83fa83d8561d2d - languageName: node - linkType: hard - "vite-plugin-node-polyfills@npm:^0.14.1": version: 0.14.1 resolution: "vite-plugin-node-polyfills@npm:0.14.1" From e14cbf563d2e6d2f62a415e21f581f1ffbc158d1 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 3 Oct 2023 12:18:11 +0200 Subject: [PATCH 116/141] chore: added chantgeset Signed-off-by: blam --- .changeset/swift-mice-care.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-mice-care.md diff --git a/.changeset/swift-mice-care.md b/.changeset/swift-mice-care.md new file mode 100644 index 0000000000..1d0d7d8703 --- /dev/null +++ b/.changeset/swift-mice-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added `EXPERIMENTAL_VITE` flag for using [vite](https://vitejs.dev) as dev server instead of Webpack From f014a14c83fdc132c4d2a6fd66942e6bb1af878e Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 3 Oct 2023 12:22:02 +0200 Subject: [PATCH 117/141] chore: remove superfluous dependency Signed-off-by: blam --- packages/cli/package.json | 1 - yarn.lock | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 86d1ae5fa9..c7d0aff18a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,7 +42,6 @@ "@backstage/types": "workspace:^", "@esbuild-kit/cjs-loader": "^2.4.1", "@esbuild-kit/esm-loader": "^2.5.5", - "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@manypkg/get-packages": "^1.1.3", "@octokit/graphql": "^5.0.0", "@octokit/graphql-schema": "^13.7.0", diff --git a/yarn.lock b/yarn.lock index 118e8dba2b..2b18799361 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3807,7 +3807,6 @@ __metadata: "@backstage/types": "workspace:^" "@esbuild-kit/cjs-loader": ^2.4.1 "@esbuild-kit/esm-loader": ^2.5.5 - "@esbuild-plugins/node-globals-polyfill": ^0.2.3 "@manypkg/get-packages": ^1.1.3 "@octokit/graphql": ^5.0.0 "@octokit/graphql-schema": ^13.7.0 @@ -10758,15 +10757,6 @@ __metadata: languageName: node linkType: hard -"@esbuild-plugins/node-globals-polyfill@npm:^0.2.3": - version: 0.2.3 - resolution: "@esbuild-plugins/node-globals-polyfill@npm:0.2.3" - peerDependencies: - esbuild: "*" - checksum: f83eeaa382680b26a3b1cf6c396450332c41d2dc0f9fd935d3f4bacf5412bef7383d2aeb4246a858781435b7c005a570dadc81051f8a038f1ef2111f17d3d8b0 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/android-arm64@npm:0.16.17" From f93f44e9265dccd1a8b1abe250f5fc1a459caf50 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 Oct 2023 10:11:24 +0200 Subject: [PATCH 118/141] chore: fixing typings Signed-off-by: blam Signed-off-by: blam --- packages/cli/src/lib/bundler/viteTransformHtml.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/bundler/viteTransformHtml.ts b/packages/cli/src/lib/bundler/viteTransformHtml.ts index 9d18e75ff9..fcbced3c40 100644 --- a/packages/cli/src/lib/bundler/viteTransformHtml.ts +++ b/packages/cli/src/lib/bundler/viteTransformHtml.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject } from '@backstage/types'; + import { PluginOption } from 'vite'; import fs from 'fs/promises'; import { render } from 'ejs'; @@ -26,7 +26,7 @@ export const viteTransformHtml = ({ }: { targetHtml: string; entryPath: string; - data: JsonObject; + data: any; }): PluginOption => ({ name: 'backstage:transform:html', configureServer(s) { From e7b7373d34c7bd03c50c5c94458e1e5170ab2b6a Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 08:47:23 +0200 Subject: [PATCH 119/141] chore: tidying up a little more and removing superfluous config Signed-off-by: blam --- packages/cli/package.json | 2 +- packages/cli/src/lib/bundler/server.ts | 33 +- .../cli/src/lib/bundler/viteTransformHtml.ts | 45 --- yarn.lock | 315 ++++-------------- 4 files changed, 81 insertions(+), 314 deletions(-) delete mode 100644 packages/cli/src/lib/bundler/viteTransformHtml.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index c7d0aff18a..e8a9c5df2d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -82,7 +82,6 @@ "css-loader": "^6.5.1", "ctrlc-windows": "^2.1.0", "diff": "^5.0.0", - "ejs": "^3.1.9", "esbuild": "^0.19.0", "esbuild-loader": "^2.18.0", "eslint": "^8.6.0", @@ -136,6 +135,7 @@ "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", "vite": "^4.4.9", + "vite-plugin-html": "^3.2.0", "vite-plugin-node-polyfills": "^0.14.1", "vite-plugin-svgr": "^4.0.0", "webpack": "^5.70.0", diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 8ccb7991ff..e1da630c53 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -24,6 +24,10 @@ import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; import vite from 'vite'; import react from '@vitejs/plugin-react'; +import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; +import { createHtmlPlugin } from 'vite-plugin-html'; +import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; +import vitePluginSvgr from 'vite-plugin-svgr'; import { forbiddenDuplicatesFilter, @@ -36,10 +40,6 @@ import { createConfig, resolveBaseUrl } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths } from './paths'; import { ServeOptions } from './types'; -import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; -import { esbuildCommonjs, viteCommonjs } from '@originjs/vite-plugin-commonjs'; -import { viteTransformHtml } from './viteTransformHtml'; -import vitePluginSvgr from 'vite-plugin-svgr'; export async function serveBundle(options: ServeOptions) { const paths = resolveBundlingPaths(options); @@ -178,12 +178,14 @@ export async function serveBundle(options: ServeOptions) { vitePluginSvgr(), viteCommonjs(), viteNodePolyfills(), - viteTransformHtml({ - entryPath: paths.targetEntry, - targetHtml: paths.targetHtml, - data: { - config: frontendConfig, - publicPath: config.output?.publicPath, + createHtmlPlugin({ + entry: paths.targetEntry, + template: 'public/index.html', + inject: { + data: { + config: frontendConfig, + publicPath: config.output?.publicPath, + }, }, }), ], @@ -191,17 +193,6 @@ export async function serveBundle(options: ServeOptions) { host, port, }, - optimizeDeps: { - esbuildOptions: { - plugins: [esbuildCommonjs(['!nano-css'])], - }, - }, - build: { - commonjsOptions: { - include: ['*'], - transformMixedEsModules: true, - }, - }, publicDir: paths.targetPublic, root: paths.targetPath, }); diff --git a/packages/cli/src/lib/bundler/viteTransformHtml.ts b/packages/cli/src/lib/bundler/viteTransformHtml.ts deleted file mode 100644 index fcbced3c40..0000000000 --- a/packages/cli/src/lib/bundler/viteTransformHtml.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 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 { PluginOption } from 'vite'; -import fs from 'fs/promises'; -import { render } from 'ejs'; -import { relative } from 'path'; - -export const viteTransformHtml = ({ - targetHtml, - entryPath, - data, -}: { - targetHtml: string; - entryPath: string; - data: any; -}): PluginOption => ({ - name: 'backstage:transform:html', - configureServer(s) { - s.middlewares.use(async (req, res, next) => { - const html = await fs.readFile(targetHtml, 'utf-8'); - const rendered = `${render(html, data)} - `; - - if (req.url === '/') { - res.end(await s.transformIndexHtml(req.url, rendered)); - } else { - next(); - } - }); - }, -}); diff --git a/yarn.lock b/yarn.lock index 2b18799361..689543fa43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -3849,8 +3852,8 @@ __metadata: "@types/terser-webpack-plugin": ^5.0.4 "@types/webpack-env": ^1.15.2 "@types/yarnpkg__lockfile": ^1.1.4 - "@typescript-eslint/eslint-plugin": ^5.9.0 - "@typescript-eslint/parser": ^5.9.0 + "@typescript-eslint/eslint-plugin": 6.7.5 + "@typescript-eslint/parser": ^6.7.2 "@vitejs/plugin-react": ^4.0.4 "@yarnpkg/lockfile": ^1.1.0 "@yarnpkg/parsers": ^3.0.0-rc.4 @@ -3865,7 +3868,6 @@ __metadata: ctrlc-windows: ^2.1.0 del: ^7.0.0 diff: ^5.0.0 - ejs: ^3.1.9 esbuild: ^0.19.0 esbuild-loader: ^2.18.0 eslint: ^8.6.0 @@ -3923,6 +3925,7 @@ __metadata: type-fest: ^2.19.0 util: ^0.12.3 vite: ^4.4.9 + vite-plugin-html: ^3.2.0 vite-plugin-node-polyfills: ^0.14.1 vite-plugin-svgr: ^4.0.0 webpack: ^5.70.0 @@ -10974,13 +10977,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.15.18": - version: 0.15.18 - resolution: "@esbuild/linux-loong64@npm:0.15.18" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/linux-loong64@npm:0.16.17" @@ -15026,7 +15022,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^4.1.1, @rollup/pluginutils@npm:^4.2.1": +"@rollup/pluginutils@npm:^4.1.1, @rollup/pluginutils@npm:^4.2.0, @rollup/pluginutils@npm:^4.2.1": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" dependencies: @@ -22615,6 +22611,13 @@ __metadata: languageName: node linkType: hard +"connect-history-api-fallback@npm:^1.6.0": + version: 1.6.0 + resolution: "connect-history-api-fallback@npm:1.6.0" + checksum: 804ca2be28c999032ecd37a9f71405e5d7b7a4b3defcebbe41077bb8c5a0a150d7b59f51dcc33b2de30bc7e217a31d10f8cfad27e8e74c2fc7655eeba82d6e7e + languageName: node + linkType: hard + "connect-history-api-fallback@npm:^2.0.0": version: 2.0.0 resolution: "connect-history-api-fallback@npm:2.0.0" @@ -22632,7 +22635,7 @@ __metadata: languageName: node linkType: hard -"consola@npm:^2.15.0": +"consola@npm:^2.15.0, consola@npm:^2.15.3": version: 2.15.3 resolution: "consola@npm:2.15.3" checksum: 8ef7a09b703ec67ac5c389a372a33b6dc97eda6c9876443a60d76a3076eea0259e7f67a4e54fd5a52f97df73690822d090cf8b7e102b5761348afef7c6d03e28 @@ -23121,7 +23124,7 @@ __metadata: languageName: node linkType: hard -"css-select@npm:^4.1.3": +"css-select@npm:^4.1.3, css-select@npm:^4.2.1": version: 4.3.0 resolution: "css-select@npm:4.3.0" dependencies: @@ -24282,6 +24285,13 @@ __metadata: languageName: node linkType: hard +"dotenv-expand@npm:^8.0.2": + version: 8.0.3 + resolution: "dotenv-expand@npm:8.0.3" + checksum: 128ce90ac825b543de3ece0154a51b056ab0dc36bb26d97a68cd0b8707327ecd3c182fb6ac63b26a0fcdfa85064419906a1065cb634f1f9dc08ad311375f1fc0 + languageName: node + linkType: hard + "dotenv@npm:^16.0.0": version: 16.0.0 resolution: "dotenv@npm:16.0.0" @@ -24402,7 +24412,7 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.6, ejs@npm:^3.1.9": +"ejs@npm:^3.1.6": version: 3.1.9 resolution: "ejs@npm:3.1.9" dependencies: @@ -24773,13 +24783,6 @@ __metadata: languageName: node linkType: hard -"esbuild-android-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-android-64@npm:0.15.18" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "esbuild-android-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-android-arm64@npm:0.14.54" @@ -24787,13 +24790,6 @@ __metadata: languageName: node linkType: hard -"esbuild-android-arm64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-android-arm64@npm:0.15.18" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "esbuild-darwin-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-darwin-64@npm:0.14.54" @@ -24801,13 +24797,6 @@ __metadata: languageName: node linkType: hard -"esbuild-darwin-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-darwin-64@npm:0.15.18" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "esbuild-darwin-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-darwin-arm64@npm:0.14.54" @@ -24815,13 +24804,6 @@ __metadata: languageName: node linkType: hard -"esbuild-darwin-arm64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-darwin-arm64@npm:0.15.18" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "esbuild-freebsd-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-freebsd-64@npm:0.14.54" @@ -24829,13 +24811,6 @@ __metadata: languageName: node linkType: hard -"esbuild-freebsd-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-freebsd-64@npm:0.15.18" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "esbuild-freebsd-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-freebsd-arm64@npm:0.14.54" @@ -24843,13 +24818,6 @@ __metadata: languageName: node linkType: hard -"esbuild-freebsd-arm64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-freebsd-arm64@npm:0.15.18" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "esbuild-linux-32@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-32@npm:0.14.54" @@ -24857,13 +24825,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-32@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-32@npm:0.15.18" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "esbuild-linux-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-64@npm:0.14.54" @@ -24871,13 +24832,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-64@npm:0.15.18" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "esbuild-linux-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-arm64@npm:0.14.54" @@ -24885,13 +24839,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-arm64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-arm64@npm:0.15.18" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "esbuild-linux-arm@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-arm@npm:0.14.54" @@ -24899,13 +24846,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-arm@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-arm@npm:0.15.18" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "esbuild-linux-mips64le@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-mips64le@npm:0.14.54" @@ -24913,13 +24853,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-mips64le@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-mips64le@npm:0.15.18" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "esbuild-linux-ppc64le@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-ppc64le@npm:0.14.54" @@ -24927,13 +24860,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-ppc64le@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-ppc64le@npm:0.15.18" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "esbuild-linux-riscv64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-riscv64@npm:0.14.54" @@ -24941,13 +24867,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-riscv64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-riscv64@npm:0.15.18" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "esbuild-linux-s390x@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-s390x@npm:0.14.54" @@ -24955,13 +24874,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-s390x@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-linux-s390x@npm:0.15.18" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "esbuild-loader@npm:^2.18.0": version: 2.21.0 resolution: "esbuild-loader@npm:2.21.0" @@ -24985,13 +24897,6 @@ __metadata: languageName: node linkType: hard -"esbuild-netbsd-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-netbsd-64@npm:0.15.18" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "esbuild-openbsd-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-openbsd-64@npm:0.14.54" @@ -24999,13 +24904,6 @@ __metadata: languageName: node linkType: hard -"esbuild-openbsd-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-openbsd-64@npm:0.15.18" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "esbuild-sunos-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-sunos-64@npm:0.14.54" @@ -25013,13 +24911,6 @@ __metadata: languageName: node linkType: hard -"esbuild-sunos-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-sunos-64@npm:0.15.18" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "esbuild-windows-32@npm:0.14.54": version: 0.14.54 resolution: "esbuild-windows-32@npm:0.14.54" @@ -25027,13 +24918,6 @@ __metadata: languageName: node linkType: hard -"esbuild-windows-32@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-windows-32@npm:0.15.18" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "esbuild-windows-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-windows-64@npm:0.14.54" @@ -25041,13 +24925,6 @@ __metadata: languageName: node linkType: hard -"esbuild-windows-64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-windows-64@npm:0.15.18" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "esbuild-windows-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-windows-arm64@npm:0.14.54" @@ -25055,13 +24932,6 @@ __metadata: languageName: node linkType: hard -"esbuild-windows-arm64@npm:0.15.18": - version: 0.15.18 - resolution: "esbuild-windows-arm64@npm:0.15.18" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "esbuild@npm:^0.14.14": version: 0.14.54 resolution: "esbuild@npm:0.14.54" @@ -25213,7 +25083,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.10": +"esbuild@npm:^0.18.10, esbuild@npm:~0.18.20": version: 0.18.20 resolution: "esbuild@npm:0.18.20" dependencies: @@ -25367,83 +25237,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:~0.18.20": - version: 0.18.20 - resolution: "esbuild@npm:0.18.20" - dependencies: - "@esbuild/android-arm": 0.18.20 - "@esbuild/android-arm64": 0.18.20 - "@esbuild/android-x64": 0.18.20 - "@esbuild/darwin-arm64": 0.18.20 - "@esbuild/darwin-x64": 0.18.20 - "@esbuild/freebsd-arm64": 0.18.20 - "@esbuild/freebsd-x64": 0.18.20 - "@esbuild/linux-arm": 0.18.20 - "@esbuild/linux-arm64": 0.18.20 - "@esbuild/linux-ia32": 0.18.20 - "@esbuild/linux-loong64": 0.18.20 - "@esbuild/linux-mips64el": 0.18.20 - "@esbuild/linux-ppc64": 0.18.20 - "@esbuild/linux-riscv64": 0.18.20 - "@esbuild/linux-s390x": 0.18.20 - "@esbuild/linux-x64": 0.18.20 - "@esbuild/netbsd-x64": 0.18.20 - "@esbuild/openbsd-x64": 0.18.20 - "@esbuild/sunos-x64": 0.18.20 - "@esbuild/win32-arm64": 0.18.20 - "@esbuild/win32-ia32": 0.18.20 - "@esbuild/win32-x64": 0.18.20 - dependenciesMeta: - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 5d253614e50cdb6ec22095afd0c414f15688e7278a7eb4f3720a6dd1306b0909cf431e7b9437a90d065a31b1c57be60130f63fe3e8d0083b588571f31ee6ec7b - languageName: node - linkType: hard - "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -28464,7 +28257,7 @@ __metadata: languageName: node linkType: hard -"html-minifier-terser@npm:^6.0.2": +"html-minifier-terser@npm:^6.0.2, html-minifier-terser@npm:^6.1.0": version: 6.1.0 resolution: "html-minifier-terser@npm:6.1.0" dependencies: @@ -34205,6 +33998,16 @@ __metadata: languageName: node linkType: hard +"node-html-parser@npm:^5.3.3": + version: 5.4.2 + resolution: "node-html-parser@npm:5.4.2" + dependencies: + css-select: ^4.2.1 + he: 1.2.0 + checksum: 2d2391147c83b402786eeab95d23ea4e24ca8608e0e70a2823bfd4f2a248be13a8cc31acfd55a0109e051131e4f0c17d7ada8d999ce70ff2e342ab0110f5da59 + languageName: node + linkType: hard + "node-html-parser@npm:^6.1.1": version: 6.1.5 resolution: "node-html-parser@npm:6.1.5" @@ -35661,6 +35464,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^0.2.0": + version: 0.2.0 + resolution: "pathe@npm:0.2.0" + checksum: 9a8149ce152088f30d15b0b03a7c128ba21f16b4dc1f3f90fe38eee9f6d0f1d6da8e4e47bd2a4f9e14aaac7c30ed01cfc86216479011de2bdc598b65e6f19f41 + languageName: node + linkType: hard + "pause@npm:0.0.1": version: 0.0.1 resolution: "pause@npm:0.0.1" @@ -36445,17 +36255,6 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.1.0, postcss@npm:^8.4.21": - version: 8.4.31 - resolution: "postcss@npm:8.4.31" - dependencies: - nanoid: ^3.3.6 - picocolors: ^1.0.0 - source-map-js: ^1.0.2 - checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea - languageName: node - linkType: hard - "postcss@npm:^8.1.0, postcss@npm:^8.4.21, postcss@npm:^8.4.27": version: 8.4.31 resolution: "postcss@npm:8.4.31" @@ -42768,6 +42567,28 @@ __metadata: languageName: node linkType: hard +"vite-plugin-html@npm:^3.2.0": + version: 3.2.0 + resolution: "vite-plugin-html@npm:3.2.0" + dependencies: + "@rollup/pluginutils": ^4.2.0 + colorette: ^2.0.16 + connect-history-api-fallback: ^1.6.0 + consola: ^2.15.3 + dotenv: ^16.0.0 + dotenv-expand: ^8.0.2 + ejs: ^3.1.6 + fast-glob: ^3.2.11 + fs-extra: ^10.0.1 + html-minifier-terser: ^6.1.0 + node-html-parser: ^5.3.3 + pathe: ^0.2.0 + peerDependencies: + vite: ">=2.0.0" + checksum: f5222247b65da1c36215f0b2f509fd3975a7426b8d44546beb49f3ba51ee87b3a6b6e6afc9e7567a0d8bd1016631f2db3f934808f62a7c8f7f83fa83d8561d2d + languageName: node + linkType: hard + "vite-plugin-node-polyfills@npm:^0.14.1": version: 0.14.1 resolution: "vite-plugin-node-polyfills@npm:0.14.1" @@ -42796,8 +42617,8 @@ __metadata: linkType: hard "vite@npm:^4.4.9": - version: 4.4.9 - resolution: "vite@npm:4.4.9" + version: 4.4.11 + resolution: "vite@npm:4.4.11" dependencies: esbuild: ^0.18.10 fsevents: ~2.3.2 @@ -42831,7 +42652,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: c511024ceae39c68c7dbf2ac4381ee655cd7bb62cf43867a14798bc835d3320b8fa7867a336143c30825c191c1fb4e9aa3348fce831ab617e96203080d3d2908 + checksum: c22145c8385343a629cd546054b9da6eee60327540102bdfd1ad897fd2e78e0763ce6a18a9d84fdefde9da8fd2427d3bec9eb2697b47cf4068c7b4b52f7e3e6a languageName: node linkType: hard From 5c0e295c2a80b8df0756337cd75aa8c624868aba Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 08:48:39 +0200 Subject: [PATCH 120/141] chore: fix ejs Signed-off-by: blam --- packages/cli/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index e8a9c5df2d..aac991f7f0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -159,7 +159,6 @@ "@backstage/theme": "workspace:^", "@types/cross-spawn": "^6.0.2", "@types/diff": "^5.0.0", - "@types/ejs": "^3.1.3", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", "@types/http-proxy": "^1.17.4", From e53067c4d8b47bc1c7c6a9096e843efc6b50d937 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 09:11:51 +0200 Subject: [PATCH 121/141] chore: fix yarn.lock dirt Signed-off-by: blam --- .github/vale/Vocab/Backstage/accept.txt | 1 + yarn.lock | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index 1a028d1387..e1c65fec06 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -430,6 +430,7 @@ Valentina validator validators varchar +vite VMware Vodafone VPCs diff --git a/yarn.lock b/yarn.lock index 689543fa43..75dc30f83a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3835,7 +3835,6 @@ __metadata: "@swc/jest": ^0.2.22 "@types/cross-spawn": ^6.0.2 "@types/diff": ^5.0.0 - "@types/ejs": ^3.1.3 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.1 "@types/http-proxy": ^1.17.4 @@ -17604,13 +17603,6 @@ __metadata: languageName: node linkType: hard -"@types/ejs@npm:^3.1.3": - version: 3.1.3 - resolution: "@types/ejs@npm:3.1.3" - checksum: b1b1c6c9d331d237523ebc410789f42edcdbb1d4cdd4a7a37ac61d2ce9c3fbcfbfe7d7f1a7f61c9334812347a0036afd52258ad2198f85545ebfb26d63475a75 - languageName: node - linkType: hard - "@types/es-aggregate-error@npm:^1.0.2": version: 1.0.2 resolution: "@types/es-aggregate-error@npm:1.0.2" From 78561f8a29e67418d470f0efcf1a13fb5437485e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 10:41:06 +0200 Subject: [PATCH 122/141] chore: remove svgr support Signed-off-by: blam --- packages/cli/package.json | 2 +- packages/cli/src/lib/bundler/server.ts | 5 +- yarn.lock | 160 +++---------------------- 3 files changed, 17 insertions(+), 150 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index aac991f7f0..80db8fb005 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -137,7 +137,6 @@ "vite": "^4.4.9", "vite-plugin-html": "^3.2.0", "vite-plugin-node-polyfills": "^0.14.1", - "vite-plugin-svgr": "^4.0.0", "webpack": "^5.70.0", "webpack-dev-server": "^4.7.3", "webpack-node-externals": "^3.0.0", @@ -159,6 +158,7 @@ "@backstage/theme": "workspace:^", "@types/cross-spawn": "^6.0.2", "@types/diff": "^5.0.0", + "@types/ejs": "^3.1.3", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", "@types/http-proxy": "^1.17.4", diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index e1da630c53..219b589723 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -27,7 +27,6 @@ import react from '@vitejs/plugin-react'; import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; import { createHtmlPlugin } from 'vite-plugin-html'; import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; -import vitePluginSvgr from 'vite-plugin-svgr'; import { forbiddenDuplicatesFilter, @@ -161,6 +160,7 @@ export async function serveBundle(options: ServeOptions) { additionalEntryPoints: detectedModulesEntryPoint, }); + console.log(paths.targetHtml); if (process.env.EXPERIMENTAL_VITE) { server = await vite.createServer({ define: { @@ -175,12 +175,11 @@ export async function serveBundle(options: ServeOptions) { }, plugins: [ react(), - vitePluginSvgr(), viteCommonjs(), viteNodePolyfills(), createHtmlPlugin({ entry: paths.targetEntry, - template: 'public/index.html', + template: `public/index.html`, inject: { data: { config: frontendConfig, diff --git a/yarn.lock b/yarn.lock index 75dc30f83a..ddc4c0b2c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1827,7 +1827,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.19.6, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.20": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.19.6, @babel/core@npm:^7.22.20": version: 7.23.0 resolution: "@babel/core@npm:7.23.0" dependencies: @@ -3414,7 +3414,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.0 resolution: "@babel/types@npm:7.23.0" dependencies: @@ -3835,6 +3835,7 @@ __metadata: "@swc/jest": ^0.2.22 "@types/cross-spawn": ^6.0.2 "@types/diff": ^5.0.0 + "@types/ejs": ^3.1.3 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.1 "@types/http-proxy": ^1.17.4 @@ -3926,7 +3927,6 @@ __metadata: vite: ^4.4.9 vite-plugin-html: ^3.2.0 vite-plugin-node-polyfills: ^0.14.1 - vite-plugin-svgr: ^4.0.0 webpack: ^5.70.0 webpack-dev-server: ^4.7.3 webpack-node-externals: ^3.0.0 @@ -15031,7 +15031,7 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.4": +"@rollup/pluginutils@npm:^5.0.1": version: 5.0.4 resolution: "@rollup/pluginutils@npm:5.0.4" dependencies: @@ -16062,15 +16062,6 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0": - version: 8.0.0 - resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3fc8e35d16f5abe0af5efe5851f27581225ac405d6a1ca44cda0df064cddfcc29a428c48c2e4bef6cebf627c9ac2f652a096030edb02cf5a120ce28d3c234710 - languageName: node - linkType: hard - "@svgr/babel-plugin-add-jsx-attribute@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:6.5.1" @@ -16080,7 +16071,7 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-remove-jsx-attribute@npm:*, @svgr/babel-plugin-remove-jsx-attribute@npm:8.0.0": +"@svgr/babel-plugin-remove-jsx-attribute@npm:*": version: 8.0.0 resolution: "@svgr/babel-plugin-remove-jsx-attribute@npm:8.0.0" peerDependencies: @@ -16089,7 +16080,7 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-remove-jsx-empty-expression@npm:*, @svgr/babel-plugin-remove-jsx-empty-expression@npm:8.0.0": +"@svgr/babel-plugin-remove-jsx-empty-expression@npm:*": version: 8.0.0 resolution: "@svgr/babel-plugin-remove-jsx-empty-expression@npm:8.0.0" peerDependencies: @@ -16098,15 +16089,6 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-replace-jsx-attribute-value@npm:8.0.0": - version: 8.0.0 - resolution: "@svgr/babel-plugin-replace-jsx-attribute-value@npm:8.0.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 1edda65ef4f4dd8f021143c8ec276a08f6baa6f733b8e8ee2e7775597bf6b97afb47fdeefd579d6ae6c959fe2e634f55cd61d99377631212228c8cfb351b8921 - languageName: node - linkType: hard - "@svgr/babel-plugin-replace-jsx-attribute-value@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-plugin-replace-jsx-attribute-value@npm:6.5.1" @@ -16116,15 +16098,6 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-svg-dynamic-title@npm:8.0.0": - version: 8.0.0 - resolution: "@svgr/babel-plugin-svg-dynamic-title@npm:8.0.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 876cec891488992e6a9aebb8155e2bea4ec461b4718c51de36e988e00e271c6d9d01ef6be17b9effd44b2b3d7db0b41c161a5904a46ae6f38b26b387ad7f3709 - languageName: node - linkType: hard - "@svgr/babel-plugin-svg-dynamic-title@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-plugin-svg-dynamic-title@npm:6.5.1" @@ -16134,15 +16107,6 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-svg-em-dimensions@npm:8.0.0": - version: 8.0.0 - resolution: "@svgr/babel-plugin-svg-em-dimensions@npm:8.0.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: be0e2d391164428327d9ec469a52cea7d93189c6b0e2c290999e048f597d777852f701c64dca44cd45b31ed14a7f859520326e2e4ad7c3a4545d0aa235bc7e9a - languageName: node - linkType: hard - "@svgr/babel-plugin-svg-em-dimensions@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-plugin-svg-em-dimensions@npm:6.5.1" @@ -16152,15 +16116,6 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-transform-react-native-svg@npm:8.1.0": - version: 8.1.0 - resolution: "@svgr/babel-plugin-transform-react-native-svg@npm:8.1.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 85b434a57572f53bd2b9f0606f253e1fcf57b4a8c554ec3f2d43ed17f50d8cae200cb3aaf1ec9d626e1456e8b135dce530ae047eb0bed6d4bf98a752d6640459 - languageName: node - linkType: hard - "@svgr/babel-plugin-transform-react-native-svg@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-plugin-transform-react-native-svg@npm:6.5.1" @@ -16170,15 +16125,6 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-plugin-transform-svg-component@npm:8.0.0": - version: 8.0.0 - resolution: "@svgr/babel-plugin-transform-svg-component@npm:8.0.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 04e2023d75693eeb0890341c40e449881184663056c249be7e5c80168e4aabb0fadd255e8d5d2dbf54b8c2a6e700efba994377135bfa4060dc4a2e860116ef8c - languageName: node - linkType: hard - "@svgr/babel-plugin-transform-svg-component@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-plugin-transform-svg-component@npm:6.5.1" @@ -16188,24 +16134,6 @@ __metadata: languageName: node linkType: hard -"@svgr/babel-preset@npm:8.1.0": - version: 8.1.0 - resolution: "@svgr/babel-preset@npm:8.1.0" - dependencies: - "@svgr/babel-plugin-add-jsx-attribute": 8.0.0 - "@svgr/babel-plugin-remove-jsx-attribute": 8.0.0 - "@svgr/babel-plugin-remove-jsx-empty-expression": 8.0.0 - "@svgr/babel-plugin-replace-jsx-attribute-value": 8.0.0 - "@svgr/babel-plugin-svg-dynamic-title": 8.0.0 - "@svgr/babel-plugin-svg-em-dimensions": 8.0.0 - "@svgr/babel-plugin-transform-react-native-svg": 8.1.0 - "@svgr/babel-plugin-transform-svg-component": 8.0.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 3a67930f080b8891e1e8e2595716b879c944d253112bae763dce59807ba23454d162216c8d66a0a0e3d4f38a649ecd6c387e545d1e1261dd69a68e9a3392ee08 - languageName: node - linkType: hard - "@svgr/babel-preset@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/babel-preset@npm:6.5.1" @@ -16237,29 +16165,6 @@ __metadata: languageName: node linkType: hard -"@svgr/core@npm:^8.1.0": - version: 8.1.0 - resolution: "@svgr/core@npm:8.1.0" - dependencies: - "@babel/core": ^7.21.3 - "@svgr/babel-preset": 8.1.0 - camelcase: ^6.2.0 - cosmiconfig: ^8.1.3 - snake-case: ^3.0.4 - checksum: da4a12865c7dc59829d58df8bd232d6c85b7115fda40da0d2f844a1a51886e2e945560596ecfc0345d37837ac457de86a931e8b8d8550e729e0c688c02250d8a - languageName: node - linkType: hard - -"@svgr/hast-util-to-babel-ast@npm:8.0.0": - version: 8.0.0 - resolution: "@svgr/hast-util-to-babel-ast@npm:8.0.0" - dependencies: - "@babel/types": ^7.21.3 - entities: ^4.4.0 - checksum: 88401281a38bbc7527e65ff5437970414391a86158ef4b4046c89764c156d2d39ecd7cce77be8a51994c9fb3249170cb1eb8b9128b62faaa81743ef6ed3534ab - languageName: node - linkType: hard - "@svgr/hast-util-to-babel-ast@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/hast-util-to-babel-ast@npm:6.5.1" @@ -16284,20 +16189,6 @@ __metadata: languageName: node linkType: hard -"@svgr/plugin-jsx@npm:^8.1.0": - version: 8.1.0 - resolution: "@svgr/plugin-jsx@npm:8.1.0" - dependencies: - "@babel/core": ^7.21.3 - "@svgr/babel-preset": 8.1.0 - "@svgr/hast-util-to-babel-ast": 8.0.0 - svg-parser: ^2.0.4 - peerDependencies: - "@svgr/core": "*" - checksum: 0418a9780753d3544912ee2dad5d2cf8d12e1ba74df8053651b3886aeda54d5f0f7d2dece0af5e0d838332c4f139a57f0dabaa3ca1afa4d1a765efce6a7656f2 - languageName: node - linkType: hard - "@svgr/plugin-svgo@npm:6.5.x, @svgr/plugin-svgo@npm:^6.5.1": version: 6.5.1 resolution: "@svgr/plugin-svgo@npm:6.5.1" @@ -17603,6 +17494,13 @@ __metadata: languageName: node linkType: hard +"@types/ejs@npm:^3.1.3": + version: 3.1.3 + resolution: "@types/ejs@npm:3.1.3" + checksum: b1b1c6c9d331d237523ebc410789f42edcdbb1d4cdd4a7a37ac61d2ce9c3fbcfbfe7d7f1a7f61c9334812347a0036afd52258ad2198f85545ebfb26d63475a75 + languageName: node + linkType: hard + "@types/es-aggregate-error@npm:^1.0.2": version: 1.0.2 resolution: "@types/es-aggregate-error@npm:1.0.2" @@ -22869,23 +22767,6 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:^8.1.3": - version: 8.3.6 - resolution: "cosmiconfig@npm:8.3.6" - dependencies: - import-fresh: ^3.3.0 - js-yaml: ^4.1.0 - parse-json: ^5.2.0 - path-type: ^4.0.0 - peerDependencies: - typescript: ">=4.9.5" - peerDependenciesMeta: - typescript: - optional: true - checksum: dc339ebea427898c9e03bf01b56ba7afbac07fc7d2a2d5a15d6e9c14de98275a9565da949375aee1809591c152c0a3877bb86dbeaf74d5bd5aaa79955ad9e7a0 - languageName: node - linkType: hard - "cpu-features@npm:0.0.2": version: 0.0.2 resolution: "cpu-features@npm:0.0.2" @@ -28628,7 +28509,7 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": +"import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" dependencies: @@ -42595,19 +42476,6 @@ __metadata: languageName: node linkType: hard -"vite-plugin-svgr@npm:^4.0.0": - version: 4.1.0 - resolution: "vite-plugin-svgr@npm:4.1.0" - dependencies: - "@rollup/pluginutils": ^5.0.4 - "@svgr/core": ^8.1.0 - "@svgr/plugin-jsx": ^8.1.0 - peerDependencies: - vite: ^2.6.0 || 3 || 4 - checksum: b2896d851a75d86d9b21b8a64c8bc8090601d6b26fcfb4739b84314dbb8b426daa7ad451e02e54ce8ae4f8473e19990caa362e2a3e9a22ee1cea0eb27c15f018 - languageName: node - linkType: hard - "vite@npm:^4.4.9": version: 4.4.11 resolution: "vite@npm:4.4.11" From a665f954a01336aadfc819bcfdf94e637007200e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 10:42:05 +0200 Subject: [PATCH 123/141] chore: don't need templating here Signed-off-by: blam --- packages/cli/src/lib/bundler/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 219b589723..4390ffd176 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -179,7 +179,7 @@ export async function serveBundle(options: ServeOptions) { viteNodePolyfills(), createHtmlPlugin({ entry: paths.targetEntry, - template: `public/index.html`, + template: 'public/index.html', inject: { data: { config: frontendConfig, From a2cdb7c22ee7e257727f19c7f4c9d557abfefd61 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 10:48:11 +0200 Subject: [PATCH 124/141] chore: add a comment Signed-off-by: blam --- packages/cli/src/lib/bundler/server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 4390ffd176..a6fa159440 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -179,6 +179,8 @@ export async function serveBundle(options: ServeOptions) { viteNodePolyfills(), createHtmlPlugin({ entry: paths.targetEntry, + // todo(blam): we should look at contributing to the plugin here + // to support absolute paths, but works in the interim at least. template: 'public/index.html', inject: { data: { From 2d5ec21f5cc3db61a5a032489dc8442561720a41 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 10:51:45 +0200 Subject: [PATCH 125/141] chore: remove needless optional chaining Signed-off-by: blam --- packages/cli/src/lib/bundler/server.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index a6fa159440..076a54cd1a 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -93,11 +93,11 @@ export async function serveBundle(options: ServeOptions) { latestFrontendAppConfigs = appConfigs; if (server) { if ('invalidate' in server) { - server?.invalidate(); + server.invalidate(); } if ('restart' in server) { - server?.restart(); + server.restart(); } } }, @@ -138,11 +138,11 @@ export async function serveBundle(options: ServeOptions) { watch() { if (server) { if ('invalidate' in server) { - server?.invalidate(); + server.invalidate(); } if ('restart' in server) { - server?.restart(); + server.restart(); } } }, From 6436c5ac6faa5d903a2de83846fad4683e455240 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 13:13:27 +0200 Subject: [PATCH 126/141] chore: cleanup Signed-off-by: blam --- packages/cli/src/lib/bundler/server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 076a54cd1a..ece9b4a6d1 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -244,6 +244,7 @@ export async function serveBundle(options: ServeOptions) { await new Promise(async (resolve, reject) => { if (process.env.EXPERIMENTAL_VITE) { await (server as vite.ViteDevServer).listen(); + (server as vite.ViteDevServer).openBrowser(); resolve(); } else { (server as WebpackDevServer).startCallback((err?: Error) => { @@ -251,10 +252,10 @@ export async function serveBundle(options: ServeOptions) { reject(err); return; } + openBrowser(url.href); + resolve(); }); } - openBrowser(url.href); - resolve(); }); const waitForExit = async () => { From b4e21496d7e69ffe6eebe9d1dba9aac2b7cbec5c Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 16:29:35 +0200 Subject: [PATCH 127/141] feat: simplfy even further! Signed-off-by: blam --- packages/cli/package.json | 1 - packages/cli/src/lib/bundler/server.ts | 12 +- .../src/legacy/TaskPage/TaskPage.tsx | 4 +- yarn.lock | 231 ------------------ 4 files changed, 3 insertions(+), 245 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 80db8fb005..4f26a72785 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -47,7 +47,6 @@ "@octokit/graphql-schema": "^13.7.0", "@octokit/oauth-app": "^4.2.0", "@octokit/request": "^6.0.0", - "@originjs/vite-plugin-commonjs": "^1.0.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@rollup/plugin-commonjs": "^23.0.0", "@rollup/plugin-json": "^5.0.0", diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index ece9b4a6d1..ead1343f0d 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -26,7 +26,6 @@ import vite from 'vite'; import react from '@vitejs/plugin-react'; import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; import { createHtmlPlugin } from 'vite-plugin-html'; -import { viteCommonjs } from '@originjs/vite-plugin-commonjs'; import { forbiddenDuplicatesFilter, @@ -160,26 +159,19 @@ export async function serveBundle(options: ServeOptions) { additionalEntryPoints: detectedModulesEntryPoint, }); - console.log(paths.targetHtml); if (process.env.EXPERIMENTAL_VITE) { server = await vite.createServer({ define: { - global: 'globalThis', + global: 'window', 'process.argv': JSON.stringify(process.argv), 'process.env.APP_CONFIG': JSON.stringify(cliConfig.frontendAppConfigs), }, - resolve: { - alias: { - 'node-fetch': 'cross-fetch', - }, - }, plugins: [ react(), - viteCommonjs(), viteNodePolyfills(), createHtmlPlugin({ entry: paths.targetEntry, - // todo(blam): we should look at contributing to the plugin here + // todo(blam): we should look at contributing to thPe plugin here // to support absolute paths, but works in the interim at least. template: 'public/index.html', inject: { diff --git a/plugins/scaffolder/src/legacy/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/legacy/TaskPage/TaskPage.tsx index 846e5a861f..e2165ed358 100644 --- a/plugins/scaffolder/src/legacy/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/legacy/TaskPage/TaskPage.tsx @@ -64,9 +64,7 @@ import { selectedTemplateRouteRef, } from '../../routes'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; - -// typings are wrong for this library, so fallback to not parsing types. -const humanizeDuration = require('humanize-duration'); +import humanizeDuration from 'humanize-duration'; const useStyles = makeStyles((theme: Theme) => createStyles({ diff --git a/yarn.lock b/yarn.lock index ddc4c0b2c0..94655c7aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3815,7 +3815,6 @@ __metadata: "@octokit/graphql-schema": ^13.7.0 "@octokit/oauth-app": ^4.2.0 "@octokit/request": ^6.0.0 - "@originjs/vite-plugin-commonjs": ^1.0.3 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 "@rollup/plugin-commonjs": ^23.0.0 "@rollup/plugin-json": ^5.0.0 @@ -10969,13 +10968,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.14.54": - version: 0.14.54 - resolution: "@esbuild/linux-loong64@npm:0.14.54" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/linux-loong64@npm:0.16.17" @@ -14510,15 +14502,6 @@ __metadata: languageName: node linkType: hard -"@originjs/vite-plugin-commonjs@npm:^1.0.3": - version: 1.0.3 - resolution: "@originjs/vite-plugin-commonjs@npm:1.0.3" - dependencies: - esbuild: ^0.14.14 - checksum: e4cd22a73e2be726fc78f794942333b8c6ede20c7b57edebe3d7bf52119532ef3206142f36498fbb22fb34fc706d30cd5efb10d1d84bf8d11be09ed7b90b8aaf - languageName: node - linkType: hard - "@parcel/watcher@npm:^2.1.0": version: 2.1.0 resolution: "@parcel/watcher@npm:2.1.0" @@ -24649,104 +24632,6 @@ __metadata: languageName: node linkType: hard -"esbuild-android-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-android-64@npm:0.14.54" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - -"esbuild-android-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-android-arm64@npm:0.14.54" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-darwin-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-darwin-64@npm:0.14.54" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"esbuild-darwin-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-darwin-arm64@npm:0.14.54" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-freebsd-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-freebsd-64@npm:0.14.54" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-freebsd-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-freebsd-arm64@npm:0.14.54" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-32@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-32@npm:0.14.54" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-linux-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-64@npm:0.14.54" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"esbuild-linux-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-arm64@npm:0.14.54" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-arm@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-arm@npm:0.14.54" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"esbuild-linux-mips64le@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-mips64le@npm:0.14.54" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"esbuild-linux-ppc64le@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-ppc64le@npm:0.14.54" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"esbuild-linux-riscv64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-riscv64@npm:0.14.54" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - -"esbuild-linux-s390x@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-s390x@npm:0.14.54" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "esbuild-loader@npm:^2.18.0": version: 2.21.0 resolution: "esbuild-loader@npm:2.21.0" @@ -24763,122 +24648,6 @@ __metadata: languageName: node linkType: hard -"esbuild-netbsd-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-netbsd-64@npm:0.14.54" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-openbsd-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-openbsd-64@npm:0.14.54" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-sunos-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-sunos-64@npm:0.14.54" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-32@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-windows-32@npm:0.14.54" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-windows-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-windows-64@npm:0.14.54" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-windows-arm64@npm:0.14.54" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"esbuild@npm:^0.14.14": - version: 0.14.54 - resolution: "esbuild@npm:0.14.54" - dependencies: - "@esbuild/linux-loong64": 0.14.54 - esbuild-android-64: 0.14.54 - esbuild-android-arm64: 0.14.54 - esbuild-darwin-64: 0.14.54 - esbuild-darwin-arm64: 0.14.54 - esbuild-freebsd-64: 0.14.54 - esbuild-freebsd-arm64: 0.14.54 - esbuild-linux-32: 0.14.54 - esbuild-linux-64: 0.14.54 - esbuild-linux-arm: 0.14.54 - esbuild-linux-arm64: 0.14.54 - esbuild-linux-mips64le: 0.14.54 - esbuild-linux-ppc64le: 0.14.54 - esbuild-linux-riscv64: 0.14.54 - esbuild-linux-s390x: 0.14.54 - esbuild-netbsd-64: 0.14.54 - esbuild-openbsd-64: 0.14.54 - esbuild-sunos-64: 0.14.54 - esbuild-windows-32: 0.14.54 - esbuild-windows-64: 0.14.54 - esbuild-windows-arm64: 0.14.54 - dependenciesMeta: - "@esbuild/linux-loong64": - optional: true - esbuild-android-64: - optional: true - esbuild-android-arm64: - optional: true - esbuild-darwin-64: - optional: true - esbuild-darwin-arm64: - optional: true - esbuild-freebsd-64: - optional: true - esbuild-freebsd-arm64: - optional: true - esbuild-linux-32: - optional: true - esbuild-linux-64: - optional: true - esbuild-linux-arm: - optional: true - esbuild-linux-arm64: - optional: true - esbuild-linux-mips64le: - optional: true - esbuild-linux-ppc64le: - optional: true - esbuild-linux-riscv64: - optional: true - esbuild-linux-s390x: - optional: true - esbuild-netbsd-64: - optional: true - esbuild-openbsd-64: - optional: true - esbuild-sunos-64: - optional: true - esbuild-windows-32: - optional: true - esbuild-windows-64: - optional: true - esbuild-windows-arm64: - optional: true - bin: - esbuild: bin/esbuild - checksum: 49e360b1185c797f5ca3a7f5f0a75121494d97ddf691f65ed1796e6257d318f928342a97f559bb8eced6a90cf604dd22db4a30e0dbbf15edd9dbf22459b639af - languageName: node - linkType: hard - "esbuild@npm:^0.16.17": version: 0.16.17 resolution: "esbuild@npm:0.16.17" From 2dddc5de00b0861ad9ef380dca93f35ae7d0ece8 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 12 Oct 2023 16:42:48 +0200 Subject: [PATCH 128/141] chore: add changeset Signed-off-by: blam --- .changeset/cold-pigs-end.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/cold-pigs-end.md diff --git a/.changeset/cold-pigs-end.md b/.changeset/cold-pigs-end.md new file mode 100644 index 0000000000..13c391ea25 --- /dev/null +++ b/.changeset/cold-pigs-end.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-home': patch +--- + +Remove usages of `require` From 788eb72840dbd3b5356b551a8a9f8c7876dc6977 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 18 Oct 2023 14:03:23 +0200 Subject: [PATCH 129/141] chore: fix Signed-off-by: blam Signed-off-by: blam --- packages/cli/package.json | 1 + packages/cli/src/lib/bundler/server.ts | 15 ++-- yarn.lock | 111 ++++++++++--------------- 3 files changed, 57 insertions(+), 70 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 4f26a72785..bf5597940b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -134,6 +134,7 @@ "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", "vite": "^4.4.9", + "vite-plugin-commonjs": "^0.10.0", "vite-plugin-html": "^3.2.0", "vite-plugin-node-polyfills": "^0.14.1", "webpack": "^5.70.0", diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index ead1343f0d..14714a3d6e 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -23,9 +23,10 @@ import openBrowser from 'react-dev-utils/openBrowser'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; import vite from 'vite'; -import react from '@vitejs/plugin-react'; +import viteReact from '@vitejs/plugin-react'; +import viteCommonJs from 'vite-plugin-commonjs'; import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; -import { createHtmlPlugin } from 'vite-plugin-html'; +import { createHtmlPlugin as viteHtml } from 'vite-plugin-html'; import { forbiddenDuplicatesFilter, @@ -167,9 +168,13 @@ export async function serveBundle(options: ServeOptions) { 'process.env.APP_CONFIG': JSON.stringify(cliConfig.frontendAppConfigs), }, plugins: [ - react(), + viteReact(), viteNodePolyfills(), - createHtmlPlugin({ + viteCommonJs({ + // todo(blam): this is ugly to work around for just running in the backstage/backstage repo. + filter: id => id.endsWith('renderReactElement.ts'), + }), + viteHtml({ entry: paths.targetEntry, // todo(blam): we should look at contributing to thPe plugin here // to support absolute paths, but works in the interim at least. @@ -236,7 +241,7 @@ export async function serveBundle(options: ServeOptions) { await new Promise(async (resolve, reject) => { if (process.env.EXPERIMENTAL_VITE) { await (server as vite.ViteDevServer).listen(); - (server as vite.ViteDevServer).openBrowser(); + openBrowser(url.href); resolve(); } else { (server as WebpackDevServer).startCallback((err?: Error) => { diff --git a/yarn.lock b/yarn.lock index 94655c7aef..cab00c9c3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1800,16 +1800,6 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.10, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.8.3": - version: 7.22.13 - resolution: "@babel/code-frame@npm:7.22.13" - dependencies: - "@babel/highlight": ^7.22.13 - chalk: ^2.4.2 - checksum: 22e342c8077c8b77eeb11f554ecca2ba14153f707b85294fcf6070b6f6150aae88a7b7436dd88d8c9289970585f3fe5b9b941c5aa3aa26a6d5a8ef3f292da058 - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.8.3": version: 7.22.13 resolution: "@babel/code-frame@npm:7.22.13" @@ -1850,18 +1840,6 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.22.10, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.7.2": - version: 7.23.0 - resolution: "@babel/generator@npm:7.23.0" - dependencies: - "@babel/types": ^7.23.0 - "@jridgewell/gen-mapping": ^0.3.2 - "@jridgewell/trace-mapping": ^0.3.17 - jsesc: ^2.5.1 - checksum: 8efe24adad34300f1f8ea2add420b28171a646edc70f2a1b3e1683842f23b8b7ffa7e35ef0119294e1901f45bfea5b3dc70abe1f10a1917ccdfb41bed69be5f1 - languageName: node - linkType: hard - "@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.7.2": version: 7.23.0 resolution: "@babel/generator@npm:7.23.0" @@ -2143,15 +2121,6 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.13.16, @babel/parser@npm:^7.14.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.8, @babel/parser@npm:^7.20.15, @babel/parser@npm:^7.22.11, @babel/parser@npm:^7.22.15, @babel/parser@npm:^7.23.0": - version: 7.23.0 - resolution: "@babel/parser@npm:7.23.0" - bin: - parser: ./bin/babel-parser.js - checksum: 453fdf8b9e2c2b7d7b02139e0ce003d1af21947bbc03eb350fb248ee335c9b85e4ab41697ddbdd97079698de825a265e45a0846bb2ed47a2c7c1df833f42a354 - languageName: node - linkType: hard - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.22.5" @@ -3378,24 +3347,6 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.22.11, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.2": - version: 7.23.2 - resolution: "@babel/traverse@npm:7.23.2" - dependencies: - "@babel/code-frame": ^7.22.13 - "@babel/generator": ^7.23.0 - "@babel/helper-environment-visitor": ^7.22.20 - "@babel/helper-function-name": ^7.23.0 - "@babel/helper-hoist-variables": ^7.22.5 - "@babel/helper-split-export-declaration": ^7.22.6 - "@babel/parser": ^7.23.0 - "@babel/types": ^7.23.0 - debug: ^4.1.0 - globals: ^11.1.0 - checksum: 26a1eea0dde41ab99dde8b9773a013a0dc50324e5110a049f5d634e721ff08afffd54940b3974a20308d7952085ac769689369e9127dea655f868c0f6e1ab35d - languageName: node - linkType: hard - "@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.23.0, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.2": version: 7.23.0 resolution: "@babel/traverse@npm:7.23.0" @@ -3425,17 +3376,6 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.20.0, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.11, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": - version: 7.23.0 - resolution: "@babel/types@npm:7.23.0" - dependencies: - "@babel/helper-string-parser": ^7.22.5 - "@babel/helper-validator-identifier": ^7.22.20 - to-fast-properties: ^2.0.0 - checksum: 215fe04bd7feef79eeb4d33374b39909ce9cad1611c4135a4f7fdf41fe3280594105af6d7094354751514625ea92d0875aba355f53e86a92600f290e77b0e604 - languageName: node - linkType: hard - "@backstage/app-defaults@workspace:^, @backstage/app-defaults@workspace:packages/app-defaults": version: 0.0.0-use.local resolution: "@backstage/app-defaults@workspace:packages/app-defaults" @@ -3924,6 +3864,7 @@ __metadata: type-fest: ^2.19.0 util: ^0.12.3 vite: ^4.4.9 + vite-plugin-commonjs: ^0.10.0 vite-plugin-html: ^3.2.0 vite-plugin-node-polyfills: ^0.14.1 webpack: ^5.70.0 @@ -12803,13 +12744,20 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13": +"@jridgewell/sourcemap-codec@npm:1.4.14": version: 1.4.14 resolution: "@jridgewell/sourcemap-codec@npm:1.4.14" checksum: 61100637b6d173d3ba786a5dff019e1a74b1f394f323c1fee337ff390239f053b87266c7a948777f4b1ee68c01a8ad0ab61e5ff4abb5a012a0b091bec391ab97 languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.15": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -19601,12 +19549,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.9.0": - version: 8.9.0 - resolution: "acorn@npm:8.9.0" +"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.10.0 + resolution: "acorn@npm:8.10.0" bin: acorn: bin/acorn - checksum: 25dfb94952386ecfb847e61934de04a4e7c2dc21c2e700fc4e2ef27ce78cb717700c4c4f279cd630bb4774948633c3859fc16063ec8573bda4568e0a312e6744 + checksum: 538ba38af0cc9e5ef983aee196c4b8b4d87c0c94532334fa7e065b2c8a1f85863467bb774231aae91613fcda5e68740c15d97b1967ae3394d20faddddd8af61d languageName: node linkType: hard @@ -31881,6 +31829,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.1": + version: 0.30.5 + resolution: "magic-string@npm:0.30.5" + dependencies: + "@jridgewell/sourcemap-codec": ^1.4.15 + checksum: da10fecff0c0a7d3faf756913ce62bd6d5e7b0402be48c3b27bfd651b90e29677e279069a63b764bcdc1b8ecdcdb898f29a5c5ec510f2323e8d62ee057a6eb18 + languageName: node + linkType: hard + "make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -42209,6 +42166,30 @@ __metadata: languageName: node linkType: hard +"vite-plugin-commonjs@npm:^0.10.0": + version: 0.10.0 + resolution: "vite-plugin-commonjs@npm:0.10.0" + dependencies: + acorn: ^8.8.2 + fast-glob: ^3.2.12 + magic-string: ^0.30.1 + vite-plugin-dynamic-import: ^1.5.0 + checksum: 3cb9a78d477a3bf7b15bd39c73058c7a5277a9be68528276fa885b4abe9ace801073dc587648248bb749cf070eb817f0ec34d8d32c0c4aac4000b23048776c2b + languageName: node + linkType: hard + +"vite-plugin-dynamic-import@npm:^1.5.0": + version: 1.5.0 + resolution: "vite-plugin-dynamic-import@npm:1.5.0" + dependencies: + acorn: ^8.8.2 + es-module-lexer: ^1.2.1 + fast-glob: ^3.2.12 + magic-string: ^0.30.1 + checksum: 68efd897daa0c72a9f9d3481095dd3f7ec9a822be5a15eef60413734a4c894c5bbd1c1762f02daeef5df59cef212c9f1e7e056315e2a7780816ccafe5c74da6e + languageName: node + linkType: hard + "vite-plugin-html@npm:^3.2.0": version: 3.2.0 resolution: "vite-plugin-html@npm:3.2.0" From 1a5ecd034a1bb85d1f6841b214123e4d41895d26 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 19 Oct 2023 13:49:07 +0200 Subject: [PATCH 130/141] chore: refactor and remove cjs Signed-off-by: blam --- .changeset/cold-pigs-end.md | 7 ---- packages/cli/package.json | 1 - packages/cli/src/lib/bundler/config.ts | 10 +---- .../cli/src/lib/bundler/hasReactDomClient.ts | 23 +++++++++++ packages/cli/src/lib/bundler/server.ts | 19 ++++------ yarn.lock | 38 +------------------ 6 files changed, 34 insertions(+), 64 deletions(-) delete mode 100644 .changeset/cold-pigs-end.md create mode 100644 packages/cli/src/lib/bundler/hasReactDomClient.ts diff --git a/.changeset/cold-pigs-end.md b/.changeset/cold-pigs-end.md deleted file mode 100644 index 13c391ea25..0000000000 --- a/.changeset/cold-pigs-end.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-react': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-home': patch ---- - -Remove usages of `require` diff --git a/packages/cli/package.json b/packages/cli/package.json index bf5597940b..4f26a72785 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -134,7 +134,6 @@ "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", "vite": "^4.4.9", - "vite-plugin-commonjs": "^0.10.0", "vite-plugin-html": "^3.2.0", "vite-plugin-node-polyfills": "^0.14.1", "webpack": "^5.70.0", diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 534ffe8c58..542cc83ce9 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -39,6 +39,7 @@ import { runPlain } from '../run'; import { transforms } from './transforms'; import { version } from '../../lib/version'; import yn from 'yn'; +import { hasReactDomClient } from './hasReactDomClient'; const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; @@ -81,15 +82,6 @@ async function readBuildInfo() { }; } -function hasReactDomClient() { - try { - require.resolve('react-dom/client'); - return true; - } catch { - return false; - } -} - export async function createConfig( paths: BundlingPaths, options: BundlingOptions, diff --git a/packages/cli/src/lib/bundler/hasReactDomClient.ts b/packages/cli/src/lib/bundler/hasReactDomClient.ts new file mode 100644 index 0000000000..e7ef7bbac9 --- /dev/null +++ b/packages/cli/src/lib/bundler/hasReactDomClient.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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 function hasReactDomClient() { + try { + require.resolve('react-dom/client'); + return true; + } catch { + return false; + } +} diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 14714a3d6e..f7847c710e 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -24,7 +24,6 @@ import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; import vite from 'vite'; import viteReact from '@vitejs/plugin-react'; -import viteCommonJs from 'vite-plugin-commonjs'; import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; import { createHtmlPlugin as viteHtml } from 'vite-plugin-html'; @@ -39,6 +38,7 @@ import { createConfig, resolveBaseUrl } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths } from './paths'; import { ServeOptions } from './types'; +import { hasReactDomClient } from './hasReactDomClient'; export async function serveBundle(options: ServeOptions) { const paths = resolveBundlingPaths(options); @@ -166,14 +166,13 @@ export async function serveBundle(options: ServeOptions) { global: 'window', 'process.argv': JSON.stringify(process.argv), 'process.env.APP_CONFIG': JSON.stringify(cliConfig.frontendAppConfigs), + // This allows for conditional imports of react-dom/client, since there's no way + // to check for presence of it in source code without module resolution errors. + 'process.env.HAS_REACT_DOM_CLIENT': JSON.stringify(hasReactDomClient()), }, plugins: [ viteReact(), viteNodePolyfills(), - viteCommonJs({ - // todo(blam): this is ugly to work around for just running in the backstage/backstage repo. - filter: id => id.endsWith('renderReactElement.ts'), - }), viteHtml({ entry: paths.targetEntry, // todo(blam): we should look at contributing to thPe plugin here @@ -233,26 +232,24 @@ export async function serveBundle(options: ServeOptions) { client: { webSocketURL: 'auto://0.0.0.0:0/ws', }, - } as any, - compiler as any, + }, + compiler, ); } await new Promise(async (resolve, reject) => { if (process.env.EXPERIMENTAL_VITE) { await (server as vite.ViteDevServer).listen(); - openBrowser(url.href); - resolve(); } else { (server as WebpackDevServer).startCallback((err?: Error) => { if (err) { reject(err); return; } - openBrowser(url.href); - resolve(); }); } + openBrowser(url.href); + resolve(); }); const waitForExit = async () => { diff --git a/yarn.lock b/yarn.lock index cab00c9c3b..f82582ba47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3864,7 +3864,6 @@ __metadata: type-fest: ^2.19.0 util: ^0.12.3 vite: ^4.4.9 - vite-plugin-commonjs: ^0.10.0 vite-plugin-html: ^3.2.0 vite-plugin-node-polyfills: ^0.14.1 webpack: ^5.70.0 @@ -12751,7 +12750,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.15": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 @@ -19549,7 +19548,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.9.0": version: 8.10.0 resolution: "acorn@npm:8.10.0" bin: @@ -31829,15 +31828,6 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.1": - version: 0.30.5 - resolution: "magic-string@npm:0.30.5" - dependencies: - "@jridgewell/sourcemap-codec": ^1.4.15 - checksum: da10fecff0c0a7d3faf756913ce62bd6d5e7b0402be48c3b27bfd651b90e29677e279069a63b764bcdc1b8ecdcdb898f29a5c5ec510f2323e8d62ee057a6eb18 - languageName: node - linkType: hard - "make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -42166,30 +42156,6 @@ __metadata: languageName: node linkType: hard -"vite-plugin-commonjs@npm:^0.10.0": - version: 0.10.0 - resolution: "vite-plugin-commonjs@npm:0.10.0" - dependencies: - acorn: ^8.8.2 - fast-glob: ^3.2.12 - magic-string: ^0.30.1 - vite-plugin-dynamic-import: ^1.5.0 - checksum: 3cb9a78d477a3bf7b15bd39c73058c7a5277a9be68528276fa885b4abe9ace801073dc587648248bb749cf070eb817f0ec34d8d32c0c4aac4000b23048776c2b - languageName: node - linkType: hard - -"vite-plugin-dynamic-import@npm:^1.5.0": - version: 1.5.0 - resolution: "vite-plugin-dynamic-import@npm:1.5.0" - dependencies: - acorn: ^8.8.2 - es-module-lexer: ^1.2.1 - fast-glob: ^3.2.12 - magic-string: ^0.30.1 - checksum: 68efd897daa0c72a9f9d3481095dd3f7ec9a822be5a15eef60413734a4c894c5bbd1c1762f02daeef5df59cef212c9f1e7e056315e2a7780816ccafe5c74da6e - languageName: node - linkType: hard - "vite-plugin-html@npm:^3.2.0": version: 3.2.0 resolution: "vite-plugin-html@npm:3.2.0" From 57f765f727d73ad947dc59353f44d16061a9dc47 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Oct 2023 11:13:26 +0200 Subject: [PATCH 131/141] wip: move Signed-off-by: blam --- packages/app/package.json | 4 ++ packages/cli/package.json | 22 +++++++--- packages/cli/src/lib/bundler/server.ts | 57 ++++++++++++-------------- yarn.lock | 20 +++++++-- 4 files changed, 63 insertions(+), 40 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index df48491481..9c6b578cd8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -93,12 +93,16 @@ "@roadiehq/backstage-plugin-github-insights": "^2.0.5", "@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7", "@roadiehq/backstage-plugin-travis-ci": "^2.0.5", + "@vitejs/plugin-react": "^4.0.4", "history": "^5.0.0", "react": "^18.0.2", "react-dom": "^18.0.2", "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-use": "^17.2.4", + "vite": "^4.4.9", + "vite-plugin-html": "^3.2.0", + "vite-plugin-node-polyfills": "^0.14.1", "zen-observable": "^0.10.0" }, "devDependencies": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 4f26a72785..4f08231f7e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -68,7 +68,6 @@ "@types/webpack-env": "^1.15.2", "@typescript-eslint/eslint-plugin": "6.7.5", "@typescript-eslint/parser": "^6.7.2", - "@vitejs/plugin-react": "^4.0.4", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "^3.0.0-rc.4", "bfj": "^7.0.2", @@ -133,9 +132,6 @@ "tar": "^6.1.12", "terser-webpack-plugin": "^5.1.3", "util": "^0.12.3", - "vite": "^4.4.9", - "vite-plugin-html": "^3.2.0", - "vite-plugin-node-polyfills": "^0.14.1", "webpack": "^5.70.0", "webpack-dev-server": "^4.7.3", "webpack-node-externals": "^3.0.0", @@ -179,11 +175,27 @@ "type-fest": "^2.19.0" }, "peerDependencies": { - "@microsoft/api-extractor": "^7.21.2" + "@microsoft/api-extractor": "^7.21.2", + "@vitejs/plugin-react": "^4.0.4", + "vite": "^4.4.9", + "vite-plugin-html": "^3.2.0", + "vite-plugin-node-polyfills": "^0.14.1" }, "peerDependenciesMeta": { "@microsoft/api-extractor": { "optional": true + }, + "@vitejs/plugin-react": { + "optional": true + }, + "vite": { + "optional": true + }, + "vite-plugin-html": { + "optional": true + }, + "vite-plugin-node-polyfills": { + "optional": true } }, "files": [ diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index f7847c710e..b4d613bcb7 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -22,10 +22,6 @@ import uniq from 'lodash/uniq'; import openBrowser from 'react-dev-utils/openBrowser'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; -import vite from 'vite'; -import viteReact from '@vitejs/plugin-react'; -import { nodePolyfills as viteNodePolyfills } from 'vite-plugin-node-polyfills'; -import { createHtmlPlugin as viteHtml } from 'vite-plugin-html'; import { forbiddenDuplicatesFilter, @@ -82,7 +78,9 @@ export async function serveBundle(options: ServeOptions) { const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); - let server: WebpackDevServer | vite.ViteDevServer | undefined = undefined; + let webpackServer: WebpackDevServer | undefined = undefined; + // let viteServer: import('vite').ViteDevServer | undefined = undefined; + let latestFrontendAppConfigs: AppConfig[] = []; const cliConfig = await loadCliConfig({ @@ -91,15 +89,9 @@ export async function serveBundle(options: ServeOptions) { withFilteredKeys: true, watch(appConfigs) { latestFrontendAppConfigs = appConfigs; - if (server) { - if ('invalidate' in server) { - server.invalidate(); - } - if ('restart' in server) { - server.restart(); - } - } + webpackServer?.invalidate(); + viteServer?.restart(); }, }); latestFrontendAppConfigs = cliConfig.frontendAppConfigs; @@ -136,15 +128,8 @@ export async function serveBundle(options: ServeOptions) { config: fullConfig, targetPath: paths.targetPath, watch() { - if (server) { - if ('invalidate' in server) { - server.invalidate(); - } - - if ('restart' in server) { - server.restart(); - } - } + webpackServer?.invalidate(); + viteServer?.restart(); }, }); @@ -161,7 +146,14 @@ export async function serveBundle(options: ServeOptions) { }); if (process.env.EXPERIMENTAL_VITE) { - server = await vite.createServer({ + // const { default: vite } = await import('vite'); + // // Annoyting that this doesn't work. `package.json` is not declared in `exports`. + // // const { default: viteReact } = require('@vitejs/plugin-react'); + // const { nodePolyfills: viteNodePolyfills } = await import( + // 'vite-plugin-node-polyfills' + // ); + // const { createHtmlPlugin: viteHtml } = await import('vite-plugin-html'); + viteServer = await vite.createServer({ define: { global: 'window', 'process.argv': JSON.stringify(process.argv), @@ -196,7 +188,7 @@ export async function serveBundle(options: ServeOptions) { } else { const compiler = webpack(config); - server = new WebpackDevServer( + webpackServer = new WebpackDevServer( { hot: !process.env.CI, devMiddleware: { @@ -237,25 +229,28 @@ export async function serveBundle(options: ServeOptions) { ); } + await viteServer?.listen(); await new Promise(async (resolve, reject) => { - if (process.env.EXPERIMENTAL_VITE) { - await (server as vite.ViteDevServer).listen(); - } else { - (server as WebpackDevServer).startCallback((err?: Error) => { + if (webpackServer) { + webpackServer.startCallback((err?: Error) => { if (err) { reject(err); return; } + resolve(); }); + } else { + resolve(); } - openBrowser(url.href); - resolve(); }); + openBrowser(url.href); + const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { - server?.close(); + webpackServer?.close(); + viteServer?.close(); // exit instead of resolve. The process is shutting down and resolving a promise here logs an error process.exit(); }); diff --git a/yarn.lock b/yarn.lock index f82582ba47..0242c61e33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3793,7 +3793,6 @@ __metadata: "@types/yarnpkg__lockfile": ^1.1.4 "@typescript-eslint/eslint-plugin": 6.7.5 "@typescript-eslint/parser": ^6.7.2 - "@vitejs/plugin-react": ^4.0.4 "@yarnpkg/lockfile": ^1.1.0 "@yarnpkg/parsers": ^3.0.0-rc.4 bfj: ^7.0.2 @@ -3863,9 +3862,6 @@ __metadata: ts-node: ^10.0.0 type-fest: ^2.19.0 util: ^0.12.3 - vite: ^4.4.9 - vite-plugin-html: ^3.2.0 - vite-plugin-node-polyfills: ^0.14.1 webpack: ^5.70.0 webpack-dev-server: ^4.7.3 webpack-node-externals: ^3.0.0 @@ -3875,9 +3871,21 @@ __metadata: zod: ^3.21.4 peerDependencies: "@microsoft/api-extractor": ^7.21.2 + "@vitejs/plugin-react": ^4.0.4 + vite: ^4.4.9 + vite-plugin-html: ^3.2.0 + vite-plugin-node-polyfills: ^0.14.1 peerDependenciesMeta: "@microsoft/api-extractor": optional: true + "@vitejs/plugin-react": + optional: true + vite: + optional: true + vite-plugin-html: + optional: true + vite-plugin-node-polyfills: + optional: true bin: backstage-cli: bin/backstage-cli languageName: unknown @@ -25571,6 +25579,7 @@ __metadata: "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 + "@vitejs/plugin-react": ^4.0.4 cross-env: ^7.0.0 history: ^5.0.0 react: ^18.0.2 @@ -25578,6 +25587,9 @@ __metadata: react-router: ^6.3.0 react-router-dom: ^6.3.0 react-use: ^17.2.4 + vite: ^4.4.9 + vite-plugin-html: ^3.2.0 + vite-plugin-node-polyfills: ^0.14.1 zen-observable: ^0.10.0 languageName: unknown linkType: soft From 57d0c77b37df778b0236f193f49b4a53571c06cf Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 20 Oct 2023 14:03:35 +0200 Subject: [PATCH 132/141] chore: revert comments Signed-off-by: blam --- packages/cli/src/lib/bundler/server.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index b4d613bcb7..bd70b2b074 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -79,7 +79,7 @@ export async function serveBundle(options: ServeOptions) { const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); let webpackServer: WebpackDevServer | undefined = undefined; - // let viteServer: import('vite').ViteDevServer | undefined = undefined; + let viteServer: import('vite').ViteDevServer | undefined = undefined; let latestFrontendAppConfigs: AppConfig[] = []; @@ -146,13 +146,12 @@ export async function serveBundle(options: ServeOptions) { }); if (process.env.EXPERIMENTAL_VITE) { - // const { default: vite } = await import('vite'); - // // Annoyting that this doesn't work. `package.json` is not declared in `exports`. - // // const { default: viteReact } = require('@vitejs/plugin-react'); - // const { nodePolyfills: viteNodePolyfills } = await import( - // 'vite-plugin-node-polyfills' - // ); - // const { createHtmlPlugin: viteHtml } = await import('vite-plugin-html'); + const { default: vite } = await import('vite'); + const { default: viteReact } = await import('@vitejs/plugin-react'); + const { nodePolyfills: viteNodePolyfills } = await import( + 'vite-plugin-node-polyfills' + ); + const { createHtmlPlugin: viteHtml } = await import('vite-plugin-html'); viteServer = await vite.createServer({ define: { global: 'window', From 7cd34392f5426bc32e106a523ad2913e7fbeacc7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 20 Oct 2023 14:26:06 +0200 Subject: [PATCH 133/141] cli: fix backend start hanging Signed-off-by: Patrik Oldsberg --- .changeset/wise-waves-approve.md | 5 +++++ .../cli/src/lib/experimental/startBackendExperimental.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/wise-waves-approve.md diff --git a/.changeset/wise-waves-approve.md b/.changeset/wise-waves-approve.md new file mode 100644 index 0000000000..f543f8918c --- /dev/null +++ b/.changeset/wise-waves-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Ignore `stdin` when spawning backend child process for the `start` command. Fixing an issue where backend startup would hang. diff --git a/packages/cli/src/lib/experimental/startBackendExperimental.ts b/packages/cli/src/lib/experimental/startBackendExperimental.ts index c4271deb3d..f649c24637 100644 --- a/packages/cli/src/lib/experimental/startBackendExperimental.ts +++ b/packages/cli/src/lib/experimental/startBackendExperimental.ts @@ -95,7 +95,7 @@ export async function startBackendExperimental(options: BackendServeOptions) { process.execPath, [...loaderArgs, ...optionArgs, options.entry, ...userArgs], { - stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + stdio: ['ignore', 'inherit', 'inherit', 'ipc'], env: { ...process.env, BACKSTAGE_CLI_CHANNEL: '1', From 96bd67dbedd67b73d9c10fd8f8dd0a1df001b553 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Fri, 20 Oct 2023 09:09:46 -0400 Subject: [PATCH 134/141] Add tests for navigation scrolling logic in TechDocsReaderPageContent Signed-off-by: Sydney Achinger --- .../TechDocsReaderPageContent.test.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx index 3e5a4c73c3..401e1e0bb8 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx @@ -33,6 +33,10 @@ jest.mock('../useReaderState', () => ({ ...jest.requireActual('../useReaderState'), useReaderState: (...args: any[]) => useReaderState(...args), })); +jest.mock('@backstage/plugin-techdocs-react', () => ({ + ...jest.requireActual('@backstage/plugin-techdocs-react'), + useShadowDomStylesLoading: jest.fn().mockReturnValue(false), +})); import { TechDocsReaderPageContent } from './TechDocsReaderPageContent'; @@ -84,6 +88,10 @@ const Wrapper = ({ ); describe('', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should render techdocs page content', async () => { getEntityMetadata.mockResolvedValue(mockEntityMetadata); getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); @@ -151,4 +159,65 @@ describe('', () => { }); }); }); + + it('should scroll to hash if hash is present in url', async () => { + jest.spyOn(document, 'querySelector'); + + const mockScrollIntoView = jest.fn(); + const h2 = document.createElement('h2'); + h2.innerText = 'emojis'; + h2.id = 'emojis'; + h2.scrollIntoView = mockScrollIntoView; + const mockTechDocsPage = document.createElement('html'); + mockTechDocsPage.appendChild(h2); + + getEntityMetadata.mockResolvedValue(mockEntityMetadata); + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + useTechDocsReaderDom.mockReturnValue(mockTechDocsPage); + useReaderState.mockReturnValue({ state: 'cached' }); + + window.location.hash = '#emojis'; + + await act(async () => { + const rendered = await renderInTestApp( + + + , + ); + + await waitFor(() => { + expect( + rendered.getByTestId('techdocs-native-shadowroot'), + ).toBeInTheDocument(); + expect(mockScrollIntoView).toHaveBeenCalled(); + expect(document.querySelector).not.toHaveBeenCalledWith('header'); + }); + }); + + window.location.hash = ''; + }); + + it('should scroll to header if hash is not present in url', async () => { + jest.spyOn(document, 'querySelector'); + + getEntityMetadata.mockResolvedValue(mockEntityMetadata); + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + useTechDocsReaderDom.mockReturnValue(document.createElement('html')); + useReaderState.mockReturnValue({ state: 'cached' }); + + await act(async () => { + const rendered = await renderInTestApp( + + + , + ); + + await waitFor(() => { + expect( + rendered.getByTestId('techdocs-native-shadowroot'), + ).toBeInTheDocument(); + expect(document.querySelector).toHaveBeenCalledWith('header'); + }); + }); + }); }); From f8727ad228c9f90202c944e6e5c802b334557186 Mon Sep 17 00:00:00 2001 From: parmar-abhinav Date: Fri, 20 Oct 2023 19:36:29 +0530 Subject: [PATCH 135/141] Add examples for publish:github:pull-request scaffolder action Signed-off-by: parmar-abhinav --- .changeset/wise-weeks-design.md | 5 + .../githubPullRequest.examples.test.ts | 552 ++++++++++++++++++ .../publish/githubPullRequest.examples.ts | 206 +++++++ .../builtin/publish/githubPullRequest.ts | 2 + 4 files changed, 765 insertions(+) create mode 100644 .changeset/wise-weeks-design.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.ts diff --git a/.changeset/wise-weeks-design.md b/.changeset/wise-weeks-design.md new file mode 100644 index 0000000000..657a8e23d5 --- /dev/null +++ b/.changeset/wise-weeks-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add examples for `publish:github:pull-request` scaffolder action & improve related tests diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.test.ts new file mode 100644 index 0000000000..3f13f2ab67 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.test.ts @@ -0,0 +1,552 @@ +/* + * Copyright 2023 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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { + OctokitWithPullRequestPluginClient, + createPublishGithubPullRequestAction, +} from './githubPullRequest'; +import yaml from 'yaml'; +import { examples } from './githubPullRequest.examples'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +const mockOctokit = { + rest: { + pulls: { + requestReviewers: jest.fn(), + }, + }, +}; + +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('publish:github:pull-request examples', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = { + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + let fakeClient: { + createPullRequest: jest.Mock; + rest: { + pulls: { requestReviewers: jest.Mock }; + }; + }; + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + + beforeEach(() => { + mockDir.clear(); + jest.resetAllMocks(); + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + fakeClient = { + createPullRequest: jest.fn(async (_: any) => { + return { + url: 'https://api.github.com/myorg/myrepo/pull/123', + headers: {}, + status: 201, + data: { + html_url: 'https://github.com/myorg/myrepo/pull/123', + number: 123, + base: { + ref: 'main', + }, + }, + }; + }), + rest: { + pulls: { + requestReviewers: jest.fn(async (_: any) => ({ data: {} })), + }, + }, + }; + + const clientFactory = jest.fn( + async () => fakeClient as unknown as OctokitWithPullRequestPluginClient, + ); + + mockDir.setContent({ + [workspacePath]: { 'file.txt': 'Hello there!' }, + }); + + action = createPublishGithubPullRequestAction({ + integrations, + githubCredentialsProvider, + clientFactory, + }); + }); + + afterEach(jest.resetAllMocks); + + it('Create a pull request', async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with target branch name', async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + base: 'test', + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request as draft', async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: true, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with target path', async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + changes: [ + { + commit: 'Create my new app', + files: { + 'targetPath/file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with source path', async () => { + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + const input = yaml.parse(examples[4].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + changes: [ + { + commit: 'Create my new app', + files: { + 'foo.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with token', async () => { + const input = yaml.parse(examples[5].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with reviewers', async () => { + const input = yaml.parse(examples[6].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + pull_number: 123, + reviewers: ['foobar'], + }); + + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with team reviewers', async () => { + const input = yaml.parse(examples[7].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + pull_number: 123, + team_reviewers: ['team-foo'], + }); + + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with commit message', async () => { + const input = yaml.parse(examples[8].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: undefined, + changes: [ + { + commit: 'Custom commit message', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with all parameters', async () => { + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + const input = yaml.parse(examples[9].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', + draft: true, + base: 'test', + changes: [ + { + commit: 'Commit for foo changes', + files: { + 'targetPath/foo.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + pull_number: 123, + reviewers: ['foobar'], + team_reviewers: ['team-foo'], + }); + + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.ts new file mode 100644 index 0000000000..d6138b262b --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.examples.ts @@ -0,0 +1,206 @@ +/* + * Copyright 2023 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Create a pull request', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + }, + }, + ], + }), + }, + { + description: 'Create a pull request with target branch name', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest with target branch name', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + targetBranchName: 'test', + }, + }, + ], + }), + }, + { + description: 'Create a pull request as draft', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest as draft', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + draft: true, + }, + }, + ], + }), + }, + { + description: 'Create a pull request with target path', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest with target path', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + targetPath: 'targetPath', + }, + }, + ], + }), + }, + { + description: 'Create a pull request with source path', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest with source path', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + sourcePath: 'source', + }, + }, + ], + }), + }, + { + description: 'Create a pull request with token', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: 'Create a pull request with reviewers', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest with reviewers', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + reviewers: ['foobar'], + }, + }, + ], + }), + }, + { + description: 'Create a pull request with team reviewers', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest with team reviewers', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + teamReviewers: ['team-foo'], + }, + }, + ], + }), + }, + { + description: 'Create a pull request with commit message', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + commitMessage: 'Custom commit message', + }, + }, + ], + }), + }, + { + description: 'Create a pull request with all parameters', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull reuqest', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + targetBranchName: 'test', + draft: true, + targetPath: 'targetPath', + sourcePath: 'source', + token: 'gph_YourGitHubToken', + reviewers: ['foobar'], + teamReviewers: ['team-foo'], + commitMessage: 'Commit for foo changes', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 167fedc88e..a02f21cfc2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -31,6 +31,7 @@ import { serializeDirectoryContents, } from '../../../../lib/files'; import { Logger } from 'winston'; +import { examples } from './githubPullRequest.examples'; export type Encoding = 'utf-8' | 'base64'; @@ -143,6 +144,7 @@ export const createPublishGithubPullRequestAction = ( commitMessage?: string; }>({ id: 'publish:github:pull-request', + examples, schema: { input: { required: ['repoUrl', 'title', 'description', 'branchName'], From 597f3a86bfe2bf6e9b6db24c04e8a520248fb414 Mon Sep 17 00:00:00 2001 From: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:39:46 -0400 Subject: [PATCH 136/141] fix(create-app): Don't suggest install if already installed Signed-off-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- packages/create-app/src/createApp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index efa299a3c9..baf80fd4d7 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -131,7 +131,7 @@ export default async (opts: OptionValues): Promise => { ); Task.log(); Task.section('All set! Now you might want to'); - if (!opts.skipInstall) { + if (opts.skipInstall) { Task.log( ` Install the dependencies: ${chalk.cyan( `cd ${opts.path ?? answers.name} && yarn install`, From ae1602e54d814c22596a3d4e7be91e33294c2be6 Mon Sep 17 00:00:00 2001 From: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:41:05 -0400 Subject: [PATCH 137/141] chore: Add changeset Signed-off-by: Adam Harvey <33203301+adamdmharvey@users.noreply.github.com> --- .changeset/real-jars-yawn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/real-jars-yawn.md diff --git a/.changeset/real-jars-yawn.md b/.changeset/real-jars-yawn.md new file mode 100644 index 0000000000..a89a687971 --- /dev/null +++ b/.changeset/real-jars-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +If create app installs dependencies, don't suggest to user that they also need to do it. From eb1a28276de69dcf391ec4eae3bc6c442c64c034 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Fri, 20 Oct 2023 11:10:51 -0400 Subject: [PATCH 138/141] Update tests to fix build. Signed-off-by: Sydney Achinger --- .../TechDocsReaderPageContent.test.tsx | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx index 425bb61be8..adef2f64d0 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.test.tsx @@ -172,20 +172,18 @@ describe('', () => { window.location.hash = '#emojis'; - await act(async () => { - const rendered = await renderInTestApp( - - - , - ); + const rendered = await renderInTestApp( + + + , + ); - await waitFor(() => { - expect( - rendered.getByTestId('techdocs-native-shadowroot'), - ).toBeInTheDocument(); - expect(mockScrollIntoView).toHaveBeenCalled(); - expect(document.querySelector).not.toHaveBeenCalledWith('header'); - }); + await waitFor(() => { + expect( + rendered.getByTestId('techdocs-native-shadowroot'), + ).toBeInTheDocument(); + expect(mockScrollIntoView).toHaveBeenCalled(); + expect(document.querySelector).not.toHaveBeenCalledWith('header'); }); window.location.hash = ''; @@ -199,19 +197,17 @@ describe('', () => { useTechDocsReaderDom.mockReturnValue(document.createElement('html')); useReaderState.mockReturnValue({ state: 'cached' }); - await act(async () => { - const rendered = await renderInTestApp( - - - , - ); + const rendered = await renderInTestApp( + + + , + ); - await waitFor(() => { - expect( - rendered.getByTestId('techdocs-native-shadowroot'), - ).toBeInTheDocument(); - expect(document.querySelector).toHaveBeenCalledWith('header'); - }); + await waitFor(() => { + expect( + rendered.getByTestId('techdocs-native-shadowroot'), + ).toBeInTheDocument(); + expect(document.querySelector).toHaveBeenCalledWith('header'); }); }); }); From 8ff165a2b5a4580d2a2f899db1e5768dc792ead0 Mon Sep 17 00:00:00 2001 From: Sydney Achinger Date: Fri, 20 Oct 2023 14:58:51 -0400 Subject: [PATCH 139/141] Add path to dependency array to fix FireFox issue. Signed-off-by: Sydney Achinger --- .../TechDocsReaderPageContent.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx index 8508c59142..f4c5ad8745 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx @@ -81,19 +81,22 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider( setShadowRoot, } = useTechDocsReaderPage(); const dom = useTechDocsReaderDom(entityRef); + const path = window.location.pathname; const hash = window.location.hash; const isStyleLoading = useShadowDomStylesLoading(dom); const [hashElement] = useShadowRootElements([`[id="${hash.slice(1)}"]`]); useEffect(() => { + if (isStyleLoading) return; + if (hash) { - if (hashElement && !isStyleLoading) { + if (hashElement) { hashElement.scrollIntoView(); } } else { document?.querySelector('header')?.scrollIntoView(); } - }, [hash, hashElement, isStyleLoading]); + }, [path, hash, hashElement, isStyleLoading]); const handleAppend = useCallback( (newShadowRoot: ShadowRoot) => { From bc9a18d5ecde14aea43cb2d75cc99f62de395090 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 21 Oct 2023 15:43:28 +0200 Subject: [PATCH 140/141] backend-app-api: double default wrapping workaround Signed-off-by: Patrik Oldsberg --- .changeset/strong-taxis-wait.md | 5 ++++ .../src/wiring/BackstageBackend.ts | 23 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .changeset/strong-taxis-wait.md diff --git a/.changeset/strong-taxis-wait.md b/.changeset/strong-taxis-wait.md new file mode 100644 index 0000000000..13cfa9a1ba --- /dev/null +++ b/.changeset/strong-taxis-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added a workaround for double `default` wrapping when dynamically importing CommonJS modules with default exports. diff --git a/packages/backend-app-api/src/wiring/BackstageBackend.ts b/packages/backend-app-api/src/wiring/BackstageBackend.ts index 2b916908de..d523481cf7 100644 --- a/packages/backend-app-api/src/wiring/BackstageBackend.ts +++ b/packages/backend-app-api/src/wiring/BackstageBackend.ts @@ -57,7 +57,26 @@ function isPromise(value: unknown | Promise): value is Promise { } function unwrapFeature( - feature: BackendFeature | (() => BackendFeature), + feature: + | BackendFeature + | (() => BackendFeature) + | { default: BackendFeature | (() => BackendFeature) }, ): BackendFeature { - return typeof feature === 'function' ? feature() : feature; + if (typeof feature === 'function') { + return feature(); + } + if ('$$type' in feature) { + return feature; + } + // This is a workaround where default exports get transpiled to `exports['default'] = ...` + // in CommonJS modules, which in turn results in a double `{ default: { default: ... } }` nesting + // when importing using a dynamic import. + // TODO: This is a broader issue than just this piece of code, and should move away from CommonJS. + if ('default' in feature) { + const defaultFeature = feature.default; + return typeof defaultFeature === 'function' + ? defaultFeature() + : defaultFeature; + } + return feature; } From 4ba4ac351f37ffec7e09059d12639dcd95497b19 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 21 Oct 2023 15:46:56 +0200 Subject: [PATCH 141/141] cli: switch to using tsx and module loader register API Signed-off-by: Patrik Oldsberg --- .changeset/dry-days-invite.md | 5 ++ packages/cli/package.json | 3 +- .../experimental/startBackendExperimental.ts | 9 ++- yarn.lock | 75 ++++++++++--------- 4 files changed, 52 insertions(+), 40 deletions(-) create mode 100644 .changeset/dry-days-invite.md diff --git a/.changeset/dry-days-invite.md b/.changeset/dry-days-invite.md new file mode 100644 index 0000000000..dcf052acd2 --- /dev/null +++ b/.changeset/dry-days-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Switch from using deprecated `@esbuild-kit/*` packages to using `tsx`. This also switches to using the new module loader `register` API when available, avoiding the experimental warning when starting backends. diff --git a/packages/cli/package.json b/packages/cli/package.json index 419b02d929..9d8b8cfdaa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,8 +40,6 @@ "@backstage/integration": "workspace:^", "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", - "@esbuild-kit/cjs-loader": "^2.4.1", - "@esbuild-kit/esm-loader": "^2.5.5", "@manypkg/get-packages": "^1.1.3", "@octokit/graphql": "^5.0.0", "@octokit/graphql-schema": "^13.7.0", @@ -131,6 +129,7 @@ "swc-loader": "^0.2.3", "tar": "^6.1.12", "terser-webpack-plugin": "^5.1.3", + "tsx": "^3.14.0", "util": "^0.12.3", "webpack": "^5.70.0", "webpack-dev-server": "^4.7.3", diff --git a/packages/cli/src/lib/experimental/startBackendExperimental.ts b/packages/cli/src/lib/experimental/startBackendExperimental.ts index c4271deb3d..cc6d3a940e 100644 --- a/packages/cli/src/lib/experimental/startBackendExperimental.ts +++ b/packages/cli/src/lib/experimental/startBackendExperimental.ts @@ -27,11 +27,14 @@ import { isAbsolute as isAbsolutePath } from 'path'; import { paths } from '../paths'; import spawn from 'cross-spawn'; +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number); +const supportsModuleLoaderRegister = nodeMajor >= 20 && nodeMinor >= 6; + const loaderArgs = [ '--require', - require.resolve('@esbuild-kit/cjs-loader'), - '--loader', - pathToFileURL(require.resolve('@esbuild-kit/esm-loader')).toString(), // Windows prefers a URL here + require.resolve('tsx/preflight'), + supportsModuleLoaderRegister ? '--import' : '--loader', + pathToFileURL(require.resolve('tsx')).toString(), // Windows prefers a URL here ]; export async function startBackendExperimental(options: BackendServeOptions) { diff --git a/yarn.lock b/yarn.lock index 3075745d85..0da2e78684 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3726,8 +3726,6 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" - "@esbuild-kit/cjs-loader": ^2.4.1 - "@esbuild-kit/esm-loader": ^2.5.5 "@manypkg/get-packages": ^1.1.3 "@octokit/graphql": ^5.0.0 "@octokit/graphql-schema": ^13.7.0 @@ -3837,6 +3835,7 @@ __metadata: tar: ^6.1.12 terser-webpack-plugin: ^5.1.3 ts-node: ^10.0.0 + tsx: ^3.14.0 type-fest: ^2.19.0 util: ^0.12.3 webpack: ^5.70.0 @@ -10641,36 +10640,6 @@ __metadata: languageName: node linkType: hard -"@esbuild-kit/cjs-loader@npm:^2.4.1": - version: 2.4.2 - resolution: "@esbuild-kit/cjs-loader@npm:2.4.2" - dependencies: - "@esbuild-kit/core-utils": ^3.0.0 - get-tsconfig: ^4.4.0 - checksum: e346e339bfc7eff5c52c270fd0ec06a7f2341b624adfb69f84b7d83f119c35070420906f2761a0b4604e0a0ec90e35eaf12544585476c428ed6d6ee3b250c0fe - languageName: node - linkType: hard - -"@esbuild-kit/core-utils@npm:^3.0.0, @esbuild-kit/core-utils@npm:^3.3.2": - version: 3.3.2 - resolution: "@esbuild-kit/core-utils@npm:3.3.2" - dependencies: - esbuild: ~0.18.20 - source-map-support: ^0.5.21 - checksum: 62f3b97457fa4ef39d752bd2ad1c8adac08929b50c411f5259f105cc74896f1fdb3429a540aa423e8eae37f32ef44656ca21ccb9a723cd9955d65a820960ab1f - languageName: node - linkType: hard - -"@esbuild-kit/esm-loader@npm:^2.5.5": - version: 2.6.5 - resolution: "@esbuild-kit/esm-loader@npm:2.6.5" - dependencies: - "@esbuild-kit/core-utils": ^3.3.2 - get-tsconfig: ^4.7.0 - checksum: 88a27203898f14bd69f03244ae2b3bae727e00243d7801292c4da8caba69813b0978fb8245a55b83bc9b907f475ed634f094a1f44cc590c0754993f4a924cc22 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/android-arm64@npm:0.16.17" @@ -26652,7 +26621,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:2.3.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@npm:2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" dependencies: @@ -26662,7 +26631,17 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@2.3.2#~builtin, fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: latest + checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@2.3.2#~builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" dependencies: @@ -26671,6 +26650,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin, fsevents@patch:fsevents@~2.3.3#~builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=18f3a7" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -26861,7 +26849,7 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.4.0, get-tsconfig@npm:^4.7.0": +"get-tsconfig@npm:^4.7.2": version: 4.7.2 resolution: "get-tsconfig@npm:4.7.2" dependencies: @@ -40881,6 +40869,23 @@ __metadata: languageName: node linkType: hard +"tsx@npm:^3.14.0": + version: 3.14.0 + resolution: "tsx@npm:3.14.0" + dependencies: + esbuild: ~0.18.20 + fsevents: ~2.3.3 + get-tsconfig: ^4.7.2 + source-map-support: ^0.5.21 + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: afcef5d9b90b5800cf1ffb749e943f63042d78a4c0d9eef6e13e43f4ecab465d45e2c9812a2c515cbdc2ee913ff1cd01bf5c606a48013dd3ce2214a631b45557 + languageName: node + linkType: hard + "tty-browserify@npm:0.0.0": version: 0.0.0 resolution: "tty-browserify@npm:0.0.0"