diff --git a/.changeset/fast-seas-hunt.md b/.changeset/fast-seas-hunt.md new file mode 100644 index 0000000000..0dcf8daf30 --- /dev/null +++ b/.changeset/fast-seas-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +--- + +Use the email from `cfIdentity` instead of `claims` when constructing user profile in order to support Cloudflare Service Tokens. diff --git a/.changeset/four-bananas-sneeze.md b/.changeset/four-bananas-sneeze.md new file mode 100644 index 0000000000..0f496b306f --- /dev/null +++ b/.changeset/four-bananas-sneeze.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-proxy-node': minor +--- + +Added `proxyEndpointsExtensionPoint` to allow addition of proxy configuration through an extension point in the new backend system. diff --git a/.changeset/light-sloths-report.md b/.changeset/light-sloths-report.md new file mode 100644 index 0000000000..8e9c20e9a4 --- /dev/null +++ b/.changeset/light-sloths-report.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': patch +--- + +The `GerritUrlReader` can now read content from a commit and not only from the top of a branch. The +Gitiles URL must contain the full commit `SHA` hash like: `https://gerrit.com/gitiles/repo/+/2846e8dc327ae2f60249983b1c3b96f42f205bae/catalog-info.yaml`. diff --git a/.changeset/lovely-stingrays-know.md b/.changeset/lovely-stingrays-know.md new file mode 100644 index 0000000000..056b4f391d --- /dev/null +++ b/.changeset/lovely-stingrays-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added `reviewerIds` to the Gitlab Merge Request action. diff --git a/.changeset/old-waves-camp.md b/.changeset/old-waves-camp.md new file mode 100644 index 0000000000..04e3b520ae --- /dev/null +++ b/.changeset/old-waves-camp.md @@ -0,0 +1,13 @@ +--- +'@backstage/integration': patch +--- + +A new Gerrit helper function (`buildGerritGitilesArchiveUrlFromLocation`) has been added. This +constructs a Gitiles URL to download an archive. It is similar to the existing +`buildGerritGitilesArchiveUrl` but also support content referenced by a full commit `SHA`. + +**DEPRECATIONS**: The function `buildGerritGitilesArchiveUrl` is deprecated, use the +`buildGerritGitilesArchiveUrlFromLocation` function instead. + +**DEPRECATIONS**: The function `parseGerritGitilesUrl` is deprecated, use the +`parseGitilesUrlRef` function instead. diff --git a/.changeset/pre.json b/.changeset/pre.json index 47232c15e9..bb3ef0d04b 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.104", diff --git a/.changeset/purple-scissors-switch.md b/.changeset/purple-scissors-switch.md new file mode 100644 index 0000000000..fe3fd07f58 --- /dev/null +++ b/.changeset/purple-scissors-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added action `gitlab:group:migrate` to migrate projects using `bulk_import` diff --git a/.changeset/strong-schools-fix.md b/.changeset/strong-schools-fix.md new file mode 100644 index 0000000000..117d33a3eb --- /dev/null +++ b/.changeset/strong-schools-fix.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Added support for `FormDecoratorBlueprint` to create form decorators in the Scaffolder plugin diff --git a/.changeset/swift-islands-burn.md b/.changeset/swift-islands-burn.md new file mode 100644 index 0000000000..f38870143b --- /dev/null +++ b/.changeset/swift-islands-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Removed the `EXPERIMENTAL_VITE` flag for using Vite as a dev server. If you were using this feature, we recommend switching to Rspack via the `EXPERIMENTAL_RSPACK` flag. diff --git a/.changeset/tasty-chairs-beam.md b/.changeset/tasty-chairs-beam.md new file mode 100644 index 0000000000..8a64e6fd0e --- /dev/null +++ b/.changeset/tasty-chairs-beam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-backstage-openapi': patch +--- + +docs: Use a valid configuration as example diff --git a/.changeset/thick-radios-wink.md b/.changeset/thick-radios-wink.md new file mode 100644 index 0000000000..25b12cc09f --- /dev/null +++ b/.changeset/thick-radios-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +--- + +Updated the `getFile` method in `BitbucketServerClient` to use `this.config.apiBaseUrl` directly for constructing the API request URL, removing the creation of an unnecessary `URL` object. The method now relies on REST API paths for accessing resources instead of direct access, ensuring better alignment with Bitbucket's API conventions. diff --git a/.changeset/young-forks-grab.md b/.changeset/young-forks-grab.md new file mode 100644 index 0000000000..8cc5f96600 --- /dev/null +++ b/.changeset/young-forks-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Updated the path field in the `gitlab:group:ensureExists` action to accept an array of either strings or objects with name and slug fields. diff --git a/OWNERS.md b/OWNERS.md index 0932d3386b..15e989f3b7 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -132,6 +132,7 @@ Scope: Tooling and Community Repo Maintainers for the Backstage [Community Plugi | -------------------- | ------------ | ------------------------------------------- | ------------ | | André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | | Bethany Griggs | Red Hat | [BethGriggs](https://github.com/BethGriggs) | `bethgriggs` | +| Kashish Mittal | Red Hat | [04kash](https://github.com/04kash) | `kashh._.` | | Nick Boldt | Red Hat | [nickboldt](https://github.com/nickboldt) | `nboldt` | | Vincenzo Scamporlino | Spotify | [vinzscam](https://github.com/vinzscam) | `vinzscam` | diff --git a/canon-docs/src/app/globals.css b/canon-docs/src/app/globals.css index de791f589a..4b39735a22 100644 --- a/canon-docs/src/app/globals.css +++ b/canon-docs/src/app/globals.css @@ -4,6 +4,10 @@ body { background-color: var(--canon-background); color: var(--canon-text-primary); transition: background-color 0.2s ease-in-out; + + --docs-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; } iframe { diff --git a/canon-docs/src/app/layout.tsx b/canon-docs/src/app/layout.tsx index f51e130a67..6eb1856a1d 100644 --- a/canon-docs/src/app/layout.tsx +++ b/canon-docs/src/app/layout.tsx @@ -1,8 +1,9 @@ import type { Metadata } from 'next'; import { Sidebar } from '../components/Sidebar'; +import { Toolbar } from '@/components/Toolbar'; +import { Providers } from './providers'; import styles from './page.module.css'; -import { Providers } from './providers'; import './globals.css'; import '/public/core.css'; @@ -19,10 +20,6 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - // const cookieStore = await cookies(); - // const theme = cookieStore.get('theme')?.value || 'light'; - // const themeName = cookieStore.get('theme-name')?.value || 'default'; - return (
-
{children}
+
+ + {children} +
diff --git a/canon-docs/src/app/opengraph-image.jpg b/canon-docs/src/app/opengraph-image.jpg new file mode 100644 index 0000000000..4dc9025ec5 Binary files /dev/null and b/canon-docs/src/app/opengraph-image.jpg differ diff --git a/canon-docs/src/app/page.module.css b/canon-docs/src/app/page.module.css index 9119d2e9c5..b0fbed71c1 100644 --- a/canon-docs/src/app/page.module.css +++ b/canon-docs/src/app/page.module.css @@ -3,10 +3,15 @@ } .container { - padding-left: 300px; width: 100%; } +@media (min-width: 768px) { + .container { + padding-left: 300px; + } +} + .page { flex: 1; } diff --git a/canon-docs/src/components/Sidebar/Sidebar.module.css b/canon-docs/src/components/Sidebar/Sidebar.module.css index 809f1b560b..870f504d28 100644 --- a/canon-docs/src/components/Sidebar/Sidebar.module.css +++ b/canon-docs/src/components/Sidebar/Sidebar.module.css @@ -1,34 +1,34 @@ .sidebar { - position: fixed; - top: 0; - left: 0; - width: 300px; - height: 100vh; - color: var(--canon-text-primary); - background-color: var(--canon-surface-1); - border-right: 1px solid var(--canon-border-base); - padding-left: 20px; - padding-right: 20px; - transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; - overflow: hidden; + display: none; +} + +@media (min-width: 768px) { + .sidebar { + display: block; + position: fixed; + top: 0; + left: 0; + width: 300px; + height: 100vh; + color: var(--canon-text-primary); + background-color: var(--canon-surface-1); + border-right: 1px solid var(--canon-border-base); + padding-left: 20px; + padding-right: 20px; + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + overflow: hidden; + } +} + +.logoContainer { + padding-left: 6px; + padding-top: 32px; } .logo path { fill: var(--canon-text-primary); } -.content { - padding-top: 20px; -} - -.actions { - display: flex; - flex-direction: row; - gap: 12px; - margin-top: 20px; - margin-bottom: 12px; -} - .menu { display: flex; flex-direction: row; @@ -46,6 +46,13 @@ width: 100%; } +.sectionTitle { + font-family: var(--docs-font); + font-size: var(--canon-font-size-body); + font-weight: var(--canon-font-weight-bold); + padding: 12px 0; +} + .line { width: 100%; display: flex; @@ -65,3 +72,16 @@ .line.active { background-color: var(--canon-surface-2); } + +.lineTitle { + font-family: var(--docs-font); + font-size: var(--canon-font-size-body); + font-weight: var(--canon-font-weight-regular); + color: var(--canon-text-primary); +} + +.lineStatus { + font-family: var(--docs-font); + font-size: var(--canon-font-size-body); + color: var(--canon-text-secondary); +} diff --git a/canon-docs/src/components/Sidebar/docs.tsx b/canon-docs/src/components/Sidebar/docs.tsx index 0d83b9bb0f..13c1675c20 100644 --- a/canon-docs/src/components/Sidebar/docs.tsx +++ b/canon-docs/src/components/Sidebar/docs.tsx @@ -55,9 +55,7 @@ export const Docs = () => { return ( - - {section.title} - +
{section.title}
{section.content.map(item => { const isActive = pathname === `${section.url}/${item.slug}`; @@ -68,17 +66,14 @@ export const Docs = () => { key={item.slug} className={`${styles.line} ${isActive ? styles.active : ''}`} > - {item.title} - +
{item.title}
+
{item.status === 'alpha' && 'Alpha'} {item.status === 'beta' && 'Beta'} {item.status === 'inProgress' && 'In Progress'} {item.status === 'stable' && 'Stable'} {item.status === 'deprecated' && 'Deprecated'} - +
); })} diff --git a/canon-docs/src/components/Sidebar/index.tsx b/canon-docs/src/components/Sidebar/index.tsx index ecbf157d6c..455ab4be9b 100644 --- a/canon-docs/src/components/Sidebar/index.tsx +++ b/canon-docs/src/components/Sidebar/index.tsx @@ -1,5 +1,4 @@ import styles from './Sidebar.module.css'; -import { TabsVersion, TabsPages, TabsTheme } from './tabs'; import { Docs } from './docs'; import { Playground } from './playground'; import Link from 'next/link'; @@ -7,7 +6,7 @@ import Link from 'next/link'; export const Sidebar = () => { return (
-
+
{ -
- - -
- -
- - -
+
+
+ +
); diff --git a/canon-docs/src/components/Sidebar/tabs.tsx b/canon-docs/src/components/Sidebar/tabs.tsx deleted file mode 100644 index e43f9a0422..0000000000 --- a/canon-docs/src/components/Sidebar/tabs.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import styles from './Tabs.module.css'; -import { Tabs } from '@base-ui-components/react/tabs'; -import { Icon } from '../../../../packages/canon/src/components/Icon'; -import { Text } from '../../../../packages/canon/src/components/Text'; -import { usePathname } from 'next/navigation'; -import { useRouter } from 'next/navigation'; -import { usePlayground } from '@/utils/playground-context'; - -export const TabsVersion = () => { - const { selectedThemeName, setSelectedThemeName } = usePlayground(); - return ( - - - - - Theme 1 - - - - - Theme 2 - - - - - - ); -}; - -export const TabsTheme = () => { - const { selectedTheme, setSelectedTheme } = usePlayground(); - - return ( - - - - - - - - - - - - ); -}; - -export const TabsPages = () => { - const pathname = usePathname(); - const router = useRouter(); - - const onValueChange = (value: string) => { - if (value === 'docs') { - router.push('/'); - } else { - router.push('/playground'); - } - }; - - return ( - - - { - router.push('/'); - }} - > - - Documentation - - - { - router.push('/playground'); - }} - > - - Playground - - - - - - ); -}; diff --git a/canon-docs/src/components/Toolbar/index.tsx b/canon-docs/src/components/Toolbar/index.tsx new file mode 100644 index 0000000000..eac51f1857 --- /dev/null +++ b/canon-docs/src/components/Toolbar/index.tsx @@ -0,0 +1,18 @@ +import { ThemeSelector } from './theme'; +import { ThemeNameSelector } from './theme-name'; +import styles from './styles.module.css'; +import { Nav } from './nav'; + +export const Toolbar = () => { + return ( +
+
+
+
+ + +
+
+ ); +}; diff --git a/canon-docs/src/components/Toolbar/nav.module.css b/canon-docs/src/components/Toolbar/nav.module.css new file mode 100644 index 0000000000..9b90f047d3 --- /dev/null +++ b/canon-docs/src/components/Toolbar/nav.module.css @@ -0,0 +1,79 @@ +.tabs { + display: none; +} + +@media (min-width: 768px) { + .tabs { + display: block; + height: 60px; + } +} + +.tabsTheme { + width: 142px; + border-radius: 0.375rem; + background-color: var(--canon-surface-2); + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; +} + +.list { + display: flex; + position: relative; + z-index: 0; + gap: var(--canon-spacing-lg); +} + +.tab { + all: unset; + height: 60px; + font-family: var(--docs-font); + color: var(--canon-text-secondary); + font-size: var(--canon-font-size-body); + font-weight: var(--canon-font-weight-bold); + cursor: pointer; + transition: color 0.2s ease-in-out; + + &:hover { + color: var(--canon-text-primary); + } + + &[data-selected] { + color: var(--canon-text-primary); + + & p { + color: var(--canon-text-primary); + } + } + + &:focus-visible { + position: relative; + + &::before { + content: ''; + position: absolute; + inset: 0.25rem 0; + border-radius: 0.25rem; + outline: 2px solid var(--canon-surface-1); + outline-offset: -1px; + } + } +} + +.tab p { + color: var(--canon-text-secondary) !important; +} + +.indicator { + position: absolute; + z-index: -1; + left: 0; + bottom: -1px; + translate: var(--active-tab-left) -50%; + width: var(--active-tab-width); + height: 1px; + border-radius: 0.25rem; + background-color: var(--canon-text-primary); + transition-property: translate, width, background-color; + transition-duration: 200ms; + transition-timing-function: ease-in-out; +} diff --git a/canon-docs/src/components/Toolbar/nav.tsx b/canon-docs/src/components/Toolbar/nav.tsx new file mode 100644 index 0000000000..017599581e --- /dev/null +++ b/canon-docs/src/components/Toolbar/nav.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { Tabs } from '@base-ui-components/react/tabs'; +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/navigation'; +import styles from './nav.module.css'; + +export const Nav = () => { + const pathname = usePathname(); + const router = useRouter(); + + const onValueChange = (value: string) => { + if (value === 'docs') { + router.push('/'); + } else { + router.push('/playground'); + } + }; + + return ( + + + { + router.push('/'); + }} + > + Documentation + + { + router.push('/playground'); + }} + > + Playground + + + + + ); +}; diff --git a/canon-docs/src/components/Toolbar/styles.module.css b/canon-docs/src/components/Toolbar/styles.module.css new file mode 100644 index 0000000000..c960dca891 --- /dev/null +++ b/canon-docs/src/components/Toolbar/styles.module.css @@ -0,0 +1,21 @@ +.toolbar { + position: sticky; + top: 0; + left: 0; + right: 0; + z-index: 10; + background-color: var(--canon-surface-1); + border-bottom: 1px solid var(--canon-border-base); + height: 60px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--canon-spacing-md); + transition: background-color 0.2s ease-in-out; +} + +.actions { + display: flex; + align-items: center; + gap: var(--canon-spacing-sm); +} diff --git a/canon-docs/src/components/Toolbar/theme-name.module.css b/canon-docs/src/components/Toolbar/theme-name.module.css new file mode 100644 index 0000000000..6f2ceb427f --- /dev/null +++ b/canon-docs/src/components/Toolbar/theme-name.module.css @@ -0,0 +1,137 @@ +.Select { + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + height: 2rem; + padding-left: 0.875rem; + padding-right: 0.75rem; + margin: 0; + outline: 0; + border: 1px solid var(--color-gray-200); + border-radius: 0.375rem; + font-family: inherit; + font-size: 1rem; + line-height: 1.5rem; + color: var(--color-gray-900); + cursor: pointer; + user-select: none; + background-color: var(--canon-surface-2); + transition: background-color 0.2s ease-in-out; + + &:focus-visible { + outline: 2px solid var(--color-blue); + outline-offset: -1px; + } +} + +.SelectIcon { + display: flex; +} + +.SelectValue { + font-size: var(--canon-font-size-body); + font-weight: var(--canon-font-weight-regular); +} + +.Positioner { + z-index: 20; +} + +.Popup { + box-sizing: border-box; + padding-block: 0.25rem; + border-radius: 0.375rem; + background-color: canvas; + color: var(--color-gray-900); + border: 1px solid var(--canon-border-base); + padding-inline: 0.25rem; + transform-origin: var(--transform-origin); + transition: transform 150ms, opacity 150ms; + + &[data-starting-style], + &[data-ending-style] { + opacity: 0; + transform: scale(0.9); + } + + &[data-side='none'] { + transition: none; + transform: none; + opacity: 1; + } + + @media (prefers-color-scheme: light) { + outline: 1px solid var(--color-gray-200); + box-shadow: 0px 10px 15px -3px var(--color-gray-200), + 0px 4px 6px -4px var(--color-gray-200); + } + + @media (prefers-color-scheme: dark) { + outline: 1px solid var(--color-gray-300); + outline-offset: -1px; + } +} + +.Item { + box-sizing: border-box; + outline: 0; + font-size: 0.875rem; + line-height: 1rem; + padding-block: 0.5rem; + padding-left: 0.625rem; + padding-right: 1rem; + font-size: var(--canon-font-size-body); + font-weight: var(--canon-font-weight-bold); + display: grid; + gap: 0.5rem; + align-items: center; + grid-template-columns: 0.75rem 1fr; + cursor: default; + user-select: none; + border-radius: 0.25rem; + cursor: pointer; + transition: background-color 0.2s ease-in-out; + + [data-side='none'] & { + font-size: 1rem; + padding-right: 3rem; + min-width: calc(var(--anchor-width) + 1rem); + } + + &[data-highlighted] { + z-index: 0; + position: relative; + color: var(--color-gray-50); + background-color: var(--canon-surface-2); + } + + &[data-highlighted]::before { + content: ''; + z-index: -1; + position: absolute; + inset-block: 0; + inset-inline: 0.25rem; + border-radius: 0.25rem; + background-color: var(--color-gray-900); + } +} + +.ItemIndicator { + grid-column-start: 1; + width: 1rem; + height: 1rem; + display: flex; + align-items: center; +} + +.ItemIndicatorIcon { + display: block; + width: 0.75rem; + height: 0.75rem; +} + +.ItemText { + grid-column-start: 2; +} diff --git a/canon-docs/src/components/Toolbar/theme-name.tsx b/canon-docs/src/components/Toolbar/theme-name.tsx new file mode 100644 index 0000000000..57c858dd44 --- /dev/null +++ b/canon-docs/src/components/Toolbar/theme-name.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { Select } from '@base-ui-components/react/select'; +import styles from './theme-name.module.css'; +import { Icon } from '@backstage/canon'; +import { usePlayground } from '@/utils/playground-context'; + +const themes = [ + { name: 'Backstage Legacy', value: 'legacy' }, + { name: 'Backstage Default', value: 'default' }, +]; + +export const ThemeNameSelector = () => { + const { selectedThemeName, setSelectedThemeName } = usePlayground(); + + return ( + + + + + + + + + + + {themes.map(({ name, value }) => ( + + + + + + {name} + + + ))} + + + + + ); +}; diff --git a/canon-docs/src/components/Sidebar/Tabs.module.css b/canon-docs/src/components/Toolbar/theme.module.css similarity index 83% rename from canon-docs/src/components/Sidebar/Tabs.module.css rename to canon-docs/src/components/Toolbar/theme.module.css index e74b47853a..2d17ffbac2 100644 --- a/canon-docs/src/components/Sidebar/Tabs.module.css +++ b/canon-docs/src/components/Toolbar/theme.module.css @@ -6,7 +6,7 @@ } .tabsTheme { - width: 142px; + width: 100px; border-radius: 0.375rem; background-color: var(--canon-surface-2); transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; @@ -29,21 +29,17 @@ outline: 0; background: none; appearance: none; - color: var(--canon-text-secondary) !important; - font-family: inherit; - font-size: 0.75rem; - line-height: 1.25rem; - font-weight: 500; + color: var(--canon-text-secondary); user-select: none; - height: 2.25rem; + height: 2rem; flex: 1; cursor: pointer; &[data-selected] { - color: var(--canon-text-primary) !important; + color: var(--canon-text-primary); & p { - color: var(--canon-text-primary) !important; + color: var(--canon-text-primary); } } @@ -78,7 +74,7 @@ top: 50%; translate: var(--active-tab-left) -50%; width: var(--active-tab-width); - height: 1.8rem; + height: 1.5rem; border-radius: 0.25rem; background-color: var(--canon-surface-1); transition-property: translate, width, background-color; diff --git a/canon-docs/src/components/Toolbar/theme.tsx b/canon-docs/src/components/Toolbar/theme.tsx new file mode 100644 index 0000000000..94224048ea --- /dev/null +++ b/canon-docs/src/components/Toolbar/theme.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { Tabs } from '@base-ui-components/react/tabs'; +import { Icon } from '@backstage/canon'; +import { usePlayground } from '@/utils/playground-context'; +import styles from './theme.module.css'; + +export const ThemeSelector = () => { + const { selectedTheme, setSelectedTheme } = usePlayground(); + + return ( + + + + + + + + + + + + ); +}; diff --git a/canon-docs/src/mdx-components.tsx b/canon-docs/src/mdx-components.tsx index 84239d2995..308356bc67 100644 --- a/canon-docs/src/mdx-components.tsx +++ b/canon-docs/src/mdx-components.tsx @@ -1,33 +1,64 @@ +import React, { ReactNode } from 'react'; import type { MDXComponents } from 'mdx/types'; import Image, { ImageProps } from 'next/image'; -import { ReactNode } from 'react'; -import React from 'react'; import { CodeBlock } from '@/components/CodeBlock'; import { Heading } from '../../packages/canon/src/components/Heading'; -import { Text } from '../../packages/canon/src/components/Text'; import { Box } from '../../packages/canon/src/components/Box'; export function useMDXComponents(components: MDXComponents): MDXComponents { return { // Allows customizing built-in components, e.g. to add styling. h1: ({ children }) => ( - - {children as ReactNode} + +

+ {children as ReactNode} +

), h2: ({ children }) => ( - {children as ReactNode} +

+ {children as ReactNode} +

), h3: ({ children }) => ( - {children as ReactNode} +

+ {children as ReactNode} +

), p: ({ children }) => ( - {children as ReactNode} +

+ {children as ReactNode} +

), a: ({ children, href }) => ( @@ -42,6 +73,21 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { return ; }, + code: ({ children }) => ( + + {children} + + ), img: props => ( { + reg.registerInit({ + deps: { + proxyEndpoints: proxyEndpointsExtensionPoint, + }, + init: async ({ proxyEndpoints }) => { + let largerExampleAuth: string = /* exercise for the reader */; + proxyEndpoints.addProxyEndpoints({ + "/simple-example": "http://simple.example.com:8080", + "/larger-example/v1": { + target: "http://larger.example.com:8080/svc.v1", + credentials: "require", + headers: { + Authorization: largerExampleAuth + }, + }, + }); + }, + }); + }, + }), +); +``` diff --git a/docs/tooling/local-dev/linking-local-packages.md b/docs/tooling/local-dev/linking-local-packages.md index 9205195f64..c2bb55722c 100644 --- a/docs/tooling/local-dev/linking-local-packages.md +++ b/docs/tooling/local-dev/linking-local-packages.md @@ -48,9 +48,9 @@ If you are encountering errors related to React, it is likely that the versions ## Generating temporary patches -!!!info +:::info This feature is experimental and currently only supports Yarn workspaces. -!!! +::: After making local changes to a package in an external workspace you might often want to merge and deploy these changes in your own project. You can use Yarn patches for this purpose, but it can be quite cumbersome to create these patches manually. To make this process easier, you can use `yarn backstage-repo-tools generate-patch` command from the `@backstage/repo-tools` package to generate a patch and resolution entries in the external workspace. diff --git a/microsite/data/plugins/crossplane.yaml b/microsite/data/plugins/crossplane.yaml new file mode 100644 index 0000000000..0fe66ae43c --- /dev/null +++ b/microsite/data/plugins/crossplane.yaml @@ -0,0 +1,9 @@ +title: Crossplane +author: terasky.com +authorUrl: https://github.com/terasky-oss +category: Kubernetes +description: View Crossplane Resources in Backstage. +documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugins/crossplane-resources +iconUrl: https://github.com/cncf/artwork/blob/main/projects/crossplane/icon/color/crossplane-icon-color.png?raw=true +npmPackageName: '@terasky/backstage-plugin-crossplane-resources-frontend' +addedDate: '2024-12-30' diff --git a/microsite/data/plugins/glean.yaml b/microsite/data/plugins/glean.yaml new file mode 100644 index 0000000000..057ec2f4fd --- /dev/null +++ b/microsite/data/plugins/glean.yaml @@ -0,0 +1,10 @@ +--- +title: Glean +author: Mark Shields +authorUrl: https://github.com/beejiujitsu +category: Search +description: Index Backstage TechDocs-enabled Entities into Glean, the AI-powered work assistant +documentation: https://github.com/beejiujitsu/wealthsimple-backstage-plugins/tree/master/plugins/glean-backend +iconUrl: /img/glean-logo-circular-white.png +npmPackageName: '@beejiujitsu/backstage-plugin-glean-backend' +addedDate: '2024-12-30' diff --git a/microsite/data/plugins/jaegertracing.yaml b/microsite/data/plugins/jaegertracing.yaml new file mode 100644 index 0000000000..dc93193ddb --- /dev/null +++ b/microsite/data/plugins/jaegertracing.yaml @@ -0,0 +1,14 @@ +--- +title: Jaeger Tracing +author: Deepankumar +authorUrl: https://github.com/deepan10 +category: Observability +description: View Jaeger traces and trace details in Backstage components. +documentation: https://github.com/backstage/community-plugins/blob/main/workspaces/jaeger/plugins/jaeger/README.md +iconUrl: https://avatars.githubusercontent.com/u/28545596?s=200&v=4 +npmPackageName: '@backstage-community/plugin-jaeger' +tags: + - monitoring + - tracing + - observability +addedDate: '2024-12-21' diff --git a/microsite/data/plugins/kubernetes-ingestor.yaml b/microsite/data/plugins/kubernetes-ingestor.yaml new file mode 100644 index 0000000000..006333e990 --- /dev/null +++ b/microsite/data/plugins/kubernetes-ingestor.yaml @@ -0,0 +1,9 @@ +title: Kubernetes Ingestor +author: terasky.com +authorUrl: https://github.com/terasky-oss +category: Kubernetes +description: Automatically Ingest Kubernetes Workloads and Crossplane Claims as Components, and XRDs as Templates. +documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugins/kubernetes-ingestor +iconUrl: https://avatars.githubusercontent.com/u/13629408 +npmPackageName: '@terasky/backstage-plugin-kubernetes-ingestor' +addedDate: '2024-12-30' diff --git a/microsite/data/plugins/kyverno.yaml b/microsite/data/plugins/kyverno.yaml new file mode 100644 index 0000000000..63554d02fe --- /dev/null +++ b/microsite/data/plugins/kyverno.yaml @@ -0,0 +1,9 @@ +title: Kyverno Policy Reports +author: terasky.com +authorUrl: https://github.com/terasky-oss +category: Kubernetes +description: View Kyverno Policy reports for Kubernetes resources related to components in Backstage. +documentation: https://github.com/TeraSky-OSS/backstage-plugins/tree/main/plugins/kyverno-policy-reports +iconUrl: https://github.com/cncf/artwork/blob/main/projects/kyverno/icon/color/kyverno-icon-color.png?raw=true +npmPackageName: '@terasky/backstage-plugin-kyverno-policy-reports' +addedDate: '2025-01-05' diff --git a/microsite/static/img/glean-logo-circular-white.png b/microsite/static/img/glean-logo-circular-white.png new file mode 100644 index 0000000000..bc83f0a6d8 Binary files /dev/null and b/microsite/static/img/glean-logo-circular-white.png differ diff --git a/packages/app/package.json b/packages/app/package.json index 25c40c08c8..b42942cbae 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -73,18 +73,13 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", - "@modyfi/vite-plugin-yaml": "^1.1.0", "@octokit/rest": "^19.0.3", - "@vitejs/plugin-react": "^4.3.1", "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": "^5.0.0", - "vite-plugin-html": "^3.2.2", - "vite-plugin-node-polyfills": "^0.22.0", "zen-observable": "^0.10.0" }, "devDependencies": { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts index 14ca4c018e..2fbb85b9b1 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts @@ -20,7 +20,7 @@ import { registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { NotModifiedError, NotFoundError } from '@backstage/errors'; +import { NotModifiedError } from '@backstage/errors'; import { GerritIntegration, readGerritIntegrationConfig, @@ -36,22 +36,11 @@ import { GerritUrlReader } from './GerritUrlReader'; import getRawBody from 'raw-body'; const mockDir = createMockDirectory({ mockOsTmpDir: true }); -const env = process.env; -process.env = { ...env, DISABLE_GERRIT_GITILES_REQUIREMENT: '1' }; const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); -const cloneMock = jest.fn(() => Promise.resolve()); -jest.mock('./git', () => ({ - Git: { - fromAuth: () => ({ - clone: cloneMock, - }), - }, -})); - // Gerrit processor without a gitilesBaseUrl configured const gerritProcessor = new GerritUrlReader( new GerritIntegration( @@ -96,12 +85,10 @@ describe.skip('GerritUrlReader', () => { beforeEach(() => { mockDir.clear(); - process.env = { ...env, DISABLE_GERRIT_GITILES_REQUIREMENT: '1' }; }); afterAll(() => { jest.clearAllMocks(); - process.env = env; }); describe('reader factory', () => { @@ -186,7 +173,24 @@ describe.skip('GerritUrlReader', () => { const buffer = await result.buffer(); expect(buffer.toString()).toBe(responseBuffer.toString()); }); - + it('should be able to read file contents of a commit as buffer', async () => { + worker.use( + rest.get( + 'https://gerrit.com/projects/web%2Fproject/commits/f775f9119c313c7ffc890d7908a45997273434d5/files/LICENSE/content', + (_, res, ctx) => { + return res( + ctx.status(200), + ctx.body(responseBuffer.toString('base64')), + ); + }, + ), + ); + const result = await gerritProcessor.readUrl( + 'https://gerrit.com/web/project/+/f775f9119c313c7ffc890d7908a45997273434d5/LICENSE', + ); + const buffer = await result.buffer(); + expect(buffer.toString()).toBe(responseBuffer.toString()); + }); it('should be able to read file contents as stream', async () => { worker.use( rest.get( @@ -258,6 +262,7 @@ describe.skip('GerritUrlReader', () => { const treeUrlGitiles = 'https://gerrit.com/gitiles/app/web/+/refs/heads/master/'; const etag = '52432507a70b677b5674b019c9a46b2e9f29d0a1'; + const sha = 'f775f9119c313c7ffc890d7908a45997273434d5'; const mkdocsContent = 'a repo fetched using git clone'; const mdContent = 'doc'; const repoArchiveBuffer = fs.readFileSync( @@ -302,6 +307,19 @@ describe.skip('GerritUrlReader', () => { ctx.body(repoArchiveDocsBuffer), ), ), + rest.get( + `https://gerrit.com/gitiles/app/web/\\+archive/${sha}.tar.gz`, + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/x-gzip'), + ctx.set( + 'content-disposition', + 'attachment; filename=web-refs/heads/master.tar.gz', + ), + ctx.body(repoArchiveBuffer), + ), + ), ); }); @@ -330,8 +348,6 @@ describe.skip('GerritUrlReader', () => { const mdFile = await files[1].content(); expect(mdFile.toString()).toBe('site_name: Test\n'); - - expect(cloneMock).not.toHaveBeenCalled(); }); it('throws NotModifiedError for matching etags.', async () => { @@ -346,15 +362,17 @@ describe.skip('GerritUrlReader', () => { ); }); - it('throws NotFoundError if branch info not found.', async () => { + it('throws ResponseError if branch info not found.', async () => { worker.use( rest.get(branchAPIUrl, (_, res, ctx) => { return res(ctx.status(404, 'Not found.')); }), ); - await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow( - NotFoundError, + await expect( + gerritProcessor.readTree(treeUrl), + ).rejects.toMatchInlineSnapshot( + `[ResponseError: Request failed with 404 Not found.]`, ); }); @@ -386,8 +404,20 @@ describe.skip('GerritUrlReader', () => { const mdFile = await files[0].content(); expect(mdFile.toString()).toBe('# Test\n'); + }); + it('throws NotModifiedError for a known commit.', async () => { + const shaTreeUrl = `https://gerrit.com/app/web/+/${sha}/`; - expect(cloneMock).not.toHaveBeenCalled(); + await expect( + gerritProcessor.readTree(shaTreeUrl, { etag: sha }), + ).rejects.toThrow(NotModifiedError); + }); + it('can fetch files for a specifc sha.', async () => { + const response = await gerritProcessorWithGitiles.readTree( + `https://gerrit.com/gitiles/app/web/+/${sha}/`, + ); + + expect(response.etag).toBe(sha); }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts index 2ef9116208..835624627b 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.ts @@ -28,14 +28,18 @@ import { Readable } from 'stream'; import { GerritIntegration, ScmIntegrations, - buildGerritGitilesArchiveUrl, + buildGerritGitilesArchiveUrlFromLocation, getGerritBranchApiUrl, getGerritFileContentsApiUrl, getGerritRequestOptions, - parseGerritGitilesUrl, parseGerritJsonResponse, + parseGitilesUrlRef, } from '@backstage/integration'; -import { NotFoundError, NotModifiedError } from '@backstage/errors'; +import { + NotFoundError, + NotModifiedError, + ResponseError, +} from '@backstage/errors'; import { ReadTreeResponseFactory, ReaderFactory } from './types'; /** @@ -135,34 +139,9 @@ export class GerritUrlReader implements UrlReaderService { url: string, options?: UrlReaderServiceReadTreeOptions, ): Promise { - const apiUrl = getGerritBranchApiUrl(this.integration.config, url); - let response: Response; - try { - response = await fetch(apiUrl, { - method: 'GET', - ...getGerritRequestOptions(this.integration.config), - }); - } catch (e) { - throw new Error(`Unable to read branch state ${url}, ${e}`); - } + const urlRevision = await this.getRevisionForUrl(url, options); - if (response.status === 404) { - throw new NotFoundError(`Not found: ${url}`); - } - - if (!response.ok) { - throw new Error( - `${url} could not be read as ${apiUrl}, ${response.status} ${response.statusText}`, - ); - } - const branchInfo = (await parseGerritJsonResponse(response as any)) as { - revision: string; - }; - if (options?.etag === branchInfo.revision) { - throw new NotModifiedError(); - } - - return this.readTreeFromGitiles(url, branchInfo.revision, options); + return this.readTreeFromGitiles(url, urlRevision, options); } async search(): Promise { @@ -179,16 +158,10 @@ export class GerritUrlReader implements UrlReaderService { revision: string, options?: UrlReaderServiceReadTreeOptions, ) { - const { branch, filePath, project } = parseGerritGitilesUrl( + const archiveUrl = buildGerritGitilesArchiveUrlFromLocation( this.integration.config, url, ); - const archiveUrl = buildGerritGitilesArchiveUrl( - this.integration.config, - project, - branch, - filePath, - ); const archiveResponse = await fetch(archiveUrl, { ...getGerritRequestOptions(this.integration.config), // TODO(freben): The signal cast is there because pre-3.x versions of @@ -217,4 +190,41 @@ export class GerritUrlReader implements UrlReaderService { stripFirstDirectory: false, }); } + + private async getRevisionForUrl( + url: string, + options?: UrlReaderServiceReadTreeOptions, + ): Promise { + const { ref, refType } = parseGitilesUrlRef(this.integration.config, url); + // The url points to a static revision. + if (refType === 'sha') { + if (options?.etag === ref) { + throw new NotModifiedError(); + } + return ref; + } + + const apiUrl = getGerritBranchApiUrl(this.integration.config, url); + let response: Response; + try { + response = await fetch(apiUrl, { + method: 'GET', + ...getGerritRequestOptions(this.integration.config), + }); + } catch (e) { + throw new Error(`Unable to read branch state ${url}, ${e}`); + } + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + const branchInfo = (await parseGerritJsonResponse(response as any)) as { + revision: string; + }; + if (options?.etag === branchInfo.revision) { + throw new NotModifiedError(); + } + return branchInfo.revision; + } } diff --git a/packages/cli/package.json b/packages/cli/package.json index ce936223a3..605231c731 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -192,28 +192,16 @@ "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack-sources": "^3.2.3", "@types/yarnpkg__lockfile": "^1.1.4", - "@vitejs/plugin-react": "^4.3.1", "del": "^8.0.0", "msw": "^1.0.0", - "nodemon": "^3.0.1", - "vite": "^5.0.0", - "vite-plugin-html": "^3.2.2", - "vite-plugin-node-polyfills": "^0.22.0" + "nodemon": "^3.0.1" }, "peerDependencies": { - "@modyfi/vite-plugin-yaml": "^1.1.0", "@rspack/core": "^1.0.10", "@rspack/dev-server": "^1.0.9", - "@rspack/plugin-react-refresh": "^1.0.0", - "@vitejs/plugin-react": "^4.0.4", - "vite": "^5.0.0", - "vite-plugin-html": "^3.2.0", - "vite-plugin-node-polyfills": "^0.22.0" + "@rspack/plugin-react-refresh": "^1.0.0" }, "peerDependenciesMeta": { - "@modyfi/vite-plugin-yaml": { - "optional": true - }, "@rspack/core": { "optional": true }, @@ -222,18 +210,6 @@ }, "@rspack/plugin-react-refresh": { "optional": true - }, - "@vitejs/plugin-react": { - "optional": true - }, - "vite": { - "optional": true - }, - "vite-plugin-html": { - "optional": true - }, - "vite-plugin-node-polyfills": { - "optional": true } }, "configSchema": { diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 1b62c82ce5..2931b3f53e 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -27,7 +27,6 @@ import { createConfig, resolveBaseUrl, resolveEndpoint } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths'; import { ServeOptions } from './types'; -import { hasReactDomClient } from './hasReactDomClient'; export async function serveBundle(options: ServeOptions) { const paths = resolveBundlingPaths(options); @@ -54,17 +53,11 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be const { name } = await fs.readJson(libPaths.resolveTarget('package.json')); let webpackServer: WebpackDevServer | undefined = undefined; - // @ts-ignore - let viteServer: import('vite').ViteDevServer | undefined = undefined; let latestFrontendAppConfigs: AppConfig[] = []; /** Triggers a full reload of all clients */ const triggerReload = () => { - if (viteServer) { - viteServer.restart(); - } - if (webpackServer) { webpackServer.invalidate(); @@ -147,167 +140,84 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be moduleFederation: options.moduleFederation, }); - if (process.env.EXPERIMENTAL_VITE) { - const vite = require('vite') as typeof import('vite'); - const { default: viteReact } = - require('@vitejs/plugin-react') as typeof import('@vitejs/plugin-react'); - const { default: viteYaml } = - require('@modyfi/vite-plugin-yaml') as typeof import('@modyfi/vite-plugin-yaml'); - const { nodePolyfills: viteNodePolyfills } = - require('vite-plugin-node-polyfills') as typeof import('vite-plugin-node-polyfills'); - const { createHtmlPlugin: viteHtml } = - require('vite-plugin-html') as typeof import('vite-plugin-html'); + const bundler = (rspack ?? webpack) as typeof webpack; + const DevServer: typeof WebpackDevServer = rspack + ? require('@rspack/dev-server').RspackDevServer + : WebpackDevServer; - viteServer = await vite.createServer({ - define: { - '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()), - }, - optimizeDeps: { - esbuildOptions: { - plugins: [ - { - name: 'custom-define', - setup(build) { - const define = (build.initialOptions.define ||= {}); - define['process.env.HAS_REACT_DOM_CLIENT'] = JSON.stringify( - hasReactDomClient(), - ); - define['process.env.NODE_ENV'] = JSON.stringify('development'); - }, - }, - ], - }, - }, - plugins: [ - viteReact(), - viteNodePolyfills({ - include: [ - 'buffer', - 'events', - 'fs', - 'http', - 'https', - 'os', - 'path', - 'process', - 'querystring', - 'stream', - 'url', - 'util', - 'zlib', - ], - globals: { - global: true, - Buffer: true, - process: true, - }, - }), - viteYaml(), - 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. - template: 'public/index.html', - inject: { - data: { - config: frontendConfig, - publicPath: config.output?.publicPath, - }, - }, - }), - ], - server: { - host, - port, - }, - publicDir: paths.targetPublic, - root: paths.targetPath, - }); - } else { - const bundler = (rspack ?? webpack) as typeof webpack; - const DevServer: typeof WebpackDevServer = rspack - ? require('@rspack/dev-server').RspackDevServer - : WebpackDevServer; - - if (rspack) { - console.log( - chalk.yellow(`⚠️ WARNING: Using experimental RSPack dev server.`), - ); - } - - const publicPaths = await resolveOptionalBundlingPaths({ - entry: 'src/index-public-experimental', - dist: 'dist/public', - }); - if (publicPaths) { - console.log( - chalk.yellow( - `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`, - ), - ); - } - const compiler = publicPaths - ? bundler([config, await createConfig(publicPaths, commonConfigOptions)]) - : bundler(config); - - webpackServer = new DevServer( - { - 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: - options.moduleFederation?.mode === 'remote' - ? false - : { - // 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`, - }, - server: - url.protocol === 'https:' - ? { - type: 'https', - options: { - cert: fullConfig.getString('app.https.certificate.cert'), - key: fullConfig.getString('app.https.certificate.key'), - }, - } - : {}, - host, - port, - proxy: targetPkg.proxy, - // When the dev server is behind a proxy, the host and public hostname differ - allowedHosts: [url.hostname], - client: { - webSocketURL: { hostname: host, port }, - }, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', - 'Access-Control-Allow-Headers': - 'X-Requested-With, content-type, Authorization', - }, - }, - compiler, + if (rspack) { + console.log( + chalk.yellow(`⚠️ WARNING: Using experimental RSPack dev server.`), ); } - await viteServer?.listen(); + const publicPaths = await resolveOptionalBundlingPaths({ + entry: 'src/index-public-experimental', + dist: 'dist/public', + }); + if (publicPaths) { + console.log( + chalk.yellow( + `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`, + ), + ); + } + const compiler = publicPaths + ? bundler([config, await createConfig(publicPaths, commonConfigOptions)]) + : bundler(config); + + webpackServer = new DevServer( + { + 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: + options.moduleFederation?.mode === 'remote' + ? false + : { + // 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`, + }, + server: + url.protocol === 'https:' + ? { + type: 'https', + options: { + cert: fullConfig.getString('app.https.certificate.cert'), + key: fullConfig.getString('app.https.certificate.key'), + }, + } + : {}, + host, + port, + proxy: targetPkg.proxy, + // When the dev server is behind a proxy, the host and public hostname differ + allowedHosts: [url.hostname], + client: { + webSocketURL: { hostname: host, port }, + }, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': + 'X-Requested-With, content-type, Authorization', + }, + }, + compiler, + ); + await new Promise(async (resolve, reject) => { if (webpackServer) { webpackServer.startCallback((err?: Error) => { @@ -330,7 +240,6 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { webpackServer?.stop(); - viteServer?.close(); // exit instead of resolve. The process is shutting down and resolving a promise here logs an error process.exit(); }); diff --git a/packages/integration/report.api.md b/packages/integration/report.api.md index 5129dbb7c2..6bb48b8185 100644 --- a/packages/integration/report.api.md +++ b/packages/integration/report.api.md @@ -299,7 +299,7 @@ export type BitbucketServerIntegrationConfig = { password?: string; }; -// @public +// @public @deprecated export function buildGerritGitilesArchiveUrl( config: GerritIntegrationConfig, project: string, @@ -307,6 +307,12 @@ export function buildGerritGitilesArchiveUrl( filePath: string, ): string; +// @public +export function buildGerritGitilesArchiveUrlFromLocation( + config: GerritIntegrationConfig, + url: string, +): string; + // @public export class DefaultAzureCredentialsManager implements AzureCredentialsManager { static fromIntegrations( @@ -802,7 +808,7 @@ export interface IntegrationsByType { harness: ScmIntegrationsGroup; } -// @public +// @public @deprecated export function parseGerritGitilesUrl( config: GerritIntegrationConfig, url: string, diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index c031612c40..5899196f65 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -21,6 +21,7 @@ import { registerMswTestHooks } from '../helpers'; import { GerritIntegrationConfig } from './config'; import { buildGerritGitilesArchiveUrl, + buildGerritGitilesArchiveUrlFromLocation, buildGerritGitilesUrl, getGerritBranchApiUrl, getGerritCloneRepoUrl, @@ -115,6 +116,94 @@ describe('gerrit core', () => { }); }); + describe('buildGerritGitilesArchiveUrlFromLocation', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/gitiles', + }; + const configWithPath: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com/gerrit', + gitilesBaseUrl: 'https://gerrit.com/gerrit/plugins/gitiles', + }; + const configWithDedicatedGitiles: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com/gerrit', + gitilesBaseUrl: 'https://dedicated-gitiles-server.com/gerrit/gitiles', + }; + it('can create an archive url for a branch', () => { + expect( + buildGerritGitilesArchiveUrlFromLocation( + config, + 'https://gerrit.com/gitiles/repo/+/refs/heads/dev/', + ), + ).toEqual( + 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', + ); + }); + it('can create an archive url for a sha', () => { + expect( + buildGerritGitilesArchiveUrlFromLocation( + config, + 'https://gerrit.com/gitiles/repo/+/2846e8dc327ae2f60249983b1c3b96f42f205bae/', + ), + ).toEqual( + 'https://gerrit.com/gitiles/repo/+archive/2846e8dc327ae2f60249983b1c3b96f42f205bae.tar.gz', + ); + }); + it('can create an archive url for a sha with a specific directory', () => { + expect( + buildGerritGitilesArchiveUrlFromLocation( + config, + 'https://gerrit.com/gitiles/repo/+/2846e8dc327ae2f60249983b1c3b96f42f205bae/docs', + ), + ).toEqual( + 'https://gerrit.com/gitiles/repo/+archive/2846e8dc327ae2f60249983b1c3b96f42f205bae/docs.tar.gz', + ); + }); + it('can create an archive url for a specific directory', () => { + expect( + buildGerritGitilesArchiveUrlFromLocation( + config, + 'https://gerrit.com/gitiles/repo/+/refs/heads/dev/docs/', + ), + ).toEqual( + 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + it('can create an authenticated url when auth is enabled and an url-path is used', () => { + const authConfig = { + ...configWithPath, + username: 'username', + password: 'password', + }; + expect( + buildGerritGitilesArchiveUrlFromLocation( + authConfig, + 'https://gerrit.com/gerrit/plugins/gitiles/repo/+/refs/heads/dev/docs/', + ), + ).toEqual( + 'https://gerrit.com/gerrit/a/plugins/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + it('Cannot build an authenticated url when a dedicated Gitiles server is used', () => { + const authConfig = { + ...configWithDedicatedGitiles, + username: 'username', + password: 'password', + }; + expect(() => + buildGerritGitilesArchiveUrlFromLocation( + authConfig, + 'https://gerrit.com/gitiles/repo/+/refs/heads/dev/', + ), + ).toThrow( + 'Since the baseUrl (Gerrit) is not part of the gitilesBaseUrl, an authentication URL could not be constructed.', + ); + }); + }); + describe('buildGerritGitilesUrl', () => { it('can create an url from arguments', () => { const config: GerritIntegrationConfig = { @@ -426,6 +515,39 @@ describe('gerrit core', () => { 'https://gerrit.com/a/projects/web%2Fproject/branches/master/files/README.md/content', ); }); + it('can create an authenticated url for a commit.', () => { + const authConfig: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + username: 'u', + password: 'u', + }; + + const authFileContentUrl = getGerritFileContentsApiUrl( + authConfig, + 'https://gerrit.com/web/project/+/157f862803d45b9d269f0e390f88aece1ded51e8/README.md', + ); + expect(authFileContentUrl).toEqual( + 'https://gerrit.com/a/projects/web%2Fproject/commits/157f862803d45b9d269f0e390f88aece1ded51e8/files/README.md/content', + ); + }); + it('will throw for unsupported ref types (tag).', () => { + const authConfig: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + username: 'u', + password: 'u', + }; + + expect(() => + getGerritFileContentsApiUrl( + authConfig, + 'https://gerrit.com/modules/events-broker/+/refs/tags/v3.5.6/src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApi.java', + ), + ).toThrow(/gitiles ref type/); + }); }); describe('parseGerritJsonResponse', () => { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index e50ce97bbb..d44865881d 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -41,8 +41,9 @@ const GERRIT_BODY_PREFIX = ")]}'"; * * @param url - An URL pointing to a file stored in git. * @public + * @deprecated `parseGerritGitilesUrl` is deprecated. Use + * {@link parseGitilesUrlRef} instead. */ - export function parseGerritGitilesUrl( config: GerritIntegrationConfig, url: string, @@ -215,6 +216,8 @@ export function buildGerritGitilesUrl( * @param branch - The branch we will target. * @param filePath - The absolute file path. * @public + * @deprecated `buildGerritGitilesArchiveUrl` is deprecated. Use + * {@link buildGerritGitilesArchiveUrlFromLocation} instead. */ export function buildGerritGitilesArchiveUrl( config: GerritIntegrationConfig, @@ -229,6 +232,38 @@ export function buildGerritGitilesArchiveUrl( )}/${project}/+archive/refs/heads/${branch}${archiveName}`; } +/** + * Build a Gerrit Gitiles archive url from a Gitiles url. + * + * @param config - A Gerrit provider config. + * @param url - The gitiles url + * @public + */ +export function buildGerritGitilesArchiveUrlFromLocation( + config: GerritIntegrationConfig, + url: string, +): string { + const { + path: filePath, + ref, + project, + refType, + } = parseGitilesUrlRef(config, url); + const archiveName = + filePath === '/' || filePath === '' ? '.tar.gz' : `/${filePath}.tar.gz`; + if (refType === 'branch') { + return `${getGitilesAuthenticationUrl( + config, + )}/${project}/+archive/refs/heads/${ref}${archiveName}`; + } + if (refType === 'sha') { + return `${getGitilesAuthenticationUrl( + config, + )}/${project}/+archive/${ref}${archiveName}`; + } + throw new Error(`Unsupported gitiles ref type: ${refType}`); +} + /** * Return the authentication prefix. * @@ -324,13 +359,25 @@ export function getGerritFileContentsApiUrl( config: GerritIntegrationConfig, url: string, ) { - const { branch, filePath, project } = parseGerritGitilesUrl(config, url); + const { ref, refType, path, project } = parseGitilesUrlRef(config, url); - return `${config.baseUrl}${getAuthenticationPrefix( - config, - )}projects/${encodeURIComponent( - project, - )}/branches/${branch}/files/${encodeURIComponent(filePath)}/content`; + // https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-content + if (refType === 'branch') { + return `${config.baseUrl}${getAuthenticationPrefix( + config, + )}projects/${encodeURIComponent( + project, + )}/branches/${ref}/files/${encodeURIComponent(path)}/content`; + } + // https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-content-from-commit + if (refType === 'sha') { + return `${config.baseUrl}${getAuthenticationPrefix( + config, + )}projects/${encodeURIComponent( + project, + )}/commits/${ref}/files/${encodeURIComponent(path)}/content`; + } + throw new Error(`Unsupported gitiles ref type: ${refType}`); } /** diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index f2e5934c95..3df52bf02b 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -20,6 +20,7 @@ export { } from './config'; export { buildGerritGitilesArchiveUrl, + buildGerritGitilesArchiveUrlFromLocation, getGerritBranchApiUrl, getGerritCloneRepoUrl, getGerritFileContentsApiUrl, diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.test.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.test.ts index 678e513412..ea38bd5180 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.test.ts @@ -18,14 +18,14 @@ import { mockServices } from '@backstage/backend-test-utils'; import { createCloudflareAccessAuthenticator } from './authenticator'; describe('authenticator', () => { - it('createCloudflareAccessAuthenticator works', async () => { + it('works for normal users', async () => { const auth = createCloudflareAccessAuthenticator({ cache: mockServices.cache.mock(), }); const profile = await auth.defaultProfileTransform( { - cfIdentity: { name: 'Name' } as any, + cfIdentity: { name: 'Name', email: 'hello@example.com' } as any, claims: { email: 'hello@example.com' } as any, token: 'fake', }, @@ -38,4 +38,25 @@ describe('authenticator', () => { }, }); }); + + it('works for service tokens', async () => { + const auth = createCloudflareAccessAuthenticator({ + cache: mockServices.cache.mock(), + }); + + const profile = await auth.defaultProfileTransform( + { + cfIdentity: { name: 'Name', email: 'hello@example.com' } as any, + claims: {} as any, + token: 'fake', + }, + {} as any, + ); + expect(profile).toEqual({ + profile: { + displayName: 'Name', + email: 'hello@example.com', + }, + }); + }); }); diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.ts index c15a1a19a0..7b311a60f0 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/authenticator.ts @@ -38,7 +38,7 @@ export function createCloudflareAccessAuthenticator(options?: { async defaultProfileTransform(result: CloudflareAccessResult) { return { profile: { - email: result.claims.email, + email: result.cfIdentity.email, displayName: result.cfIdentity.name, }, }; diff --git a/plugins/catalog-backend-module-backstage-openapi/README.md b/plugins/catalog-backend-module-backstage-openapi/README.md index 8df5b5d6c9..c4342d42cf 100644 --- a/plugins/catalog-backend-module-backstage-openapi/README.md +++ b/plugins/catalog-backend-module-backstage-openapi/README.md @@ -26,14 +26,14 @@ catalog: backstageOpenapi: plugins: - catalog - - todo + - events - search entityOverrides: # All optional metadata: - name: 'my name' + name: 'my-name' title: 'my title' spec: - owner: 'my team' + owner: 'my-team' ``` We will attempt to load each plugin's OpenAPI spec hosted at `${pluginRoute}/openapi.json`. These are automatically added if you are using `@backstage/backend-openapi-utils`'s `createValidatedOpenApiRouter`. diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts index db2a05d0b1..6abcede9de 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts @@ -153,7 +153,7 @@ describe('BitbucketServerClient', () => { it('getFile', async () => { server.use( rest.get( - `https://${config.host}/projects/test-project/repos/test-repo/raw/catalog-info.yaml`, + `${config.apiBaseUrl}/projects/test-project/repos/test-repo/raw/catalog-info.yaml`, (req, res, ctx) => { if ( req.headers.get('authorization') !== diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts index 73373ffff1..3dd61d64e7 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.ts @@ -77,9 +77,8 @@ export class BitbucketServerClient { repo: string; path: string; }): Promise { - const base = new URL(this.config.apiBaseUrl); return throttledFetch( - `${base.protocol}//${base.host}/projects/${options.projectKey}/repos/${options.repo}/raw/${options.path}`, + `${this.config.apiBaseUrl}/projects/${options.projectKey}/repos/${options.repo}/raw/${options.path}`, getBitbucketServerRequestOptions(this.config), ); } diff --git a/plugins/proxy-backend/dev/index.ts b/plugins/proxy-backend/dev/index.ts index 43c75d24df..4e4f84056a 100644 --- a/plugins/proxy-backend/dev/index.ts +++ b/plugins/proxy-backend/dev/index.ts @@ -15,7 +15,35 @@ */ import { createBackend } from '@backstage/backend-defaults'; +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { proxyEndpointsExtensionPoint } from '@backstage/plugin-proxy-node/alpha'; const backend = createBackend(); backend.add(import('../src/alpha')); + +backend.add( + createBackendModule({ + pluginId: 'proxy', + moduleId: 'demo-additional-endpoints', + register: reg => { + reg.registerInit({ + deps: { + proxyEndpoints: proxyEndpointsExtensionPoint, + }, + init: async ({ proxyEndpoints }) => { + proxyEndpoints.addProxyEndpoints({ + ...Object.fromEntries( + ['foo', 'bar', 'baz'].map(msv => [ + `/${msv}`, + { target: `http://${msv}.org` }, + ]), + ), + '/gocd': 'http://cannot-override-config.no', + }); + }, + }); + }, + }), +); + backend.start(); diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 8f3de355bf..0498a60959 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -56,6 +56,7 @@ "@backstage/backend-common": "^0.25.0", "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", + "@backstage/plugin-proxy-node": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/proxy-backend/report.api.md b/plugins/proxy-backend/report.api.md index c0601eac30..90e7f54378 100644 --- a/plugins/proxy-backend/report.api.md +++ b/plugins/proxy-backend/report.api.md @@ -7,6 +7,7 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; import { Logger } from 'winston'; +import { ProxyConfig } from '@backstage/plugin-proxy-node/alpha'; import { RootConfigService } from '@backstage/backend-plugin-api'; // @public @deprecated @@ -18,6 +19,8 @@ export default proxyPlugin; // @public @deprecated (undocumented) export interface RouterOptions { + // (undocumented) + additionalEndpoints?: ProxyConfig; // (undocumented) config: RootConfigService; // (undocumented) diff --git a/plugins/proxy-backend/src/plugin.ts b/plugins/proxy-backend/src/plugin.ts index 1039a6a3e8..52c1c26acd 100644 --- a/plugins/proxy-backend/src/plugin.ts +++ b/plugins/proxy-backend/src/plugin.ts @@ -20,6 +20,7 @@ import { coreServices, } from '@backstage/backend-plugin-api'; import { createRouterInternal } from './service/router'; +import { proxyEndpointsExtensionPoint } from '@backstage/plugin-proxy-node/alpha'; /** * The proxy backend plugin. @@ -29,6 +30,13 @@ import { createRouterInternal } from './service/router'; export const proxyPlugin = createBackendPlugin({ pluginId: 'proxy', register(env) { + const additionalEndpoints = {}; + + env.registerExtensionPoint(proxyEndpointsExtensionPoint, { + addProxyEndpoints(endpoints) { + Object.assign(additionalEndpoints, endpoints); + }, + }); env.registerInit({ deps: { config: coreServices.rootConfig, @@ -42,6 +50,7 @@ export const proxyPlugin = createBackendPlugin({ discovery, logger: loggerToWinstonLogger(logger), httpRouterService: httpRouter, + additionalEndpoints, }); }, }); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index 9ea064ea55..934b1fcecb 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -20,7 +20,6 @@ import Router from 'express-promise-router'; import { createProxyMiddleware, fixRequestBody, - Options, RequestHandler, } from 'http-proxy-middleware'; import { Logger } from 'winston'; @@ -31,6 +30,7 @@ import { HttpRouterService, RootConfigService, } from '@backstage/backend-plugin-api'; +import { ProxyConfig } from '@backstage/plugin-proxy-node/alpha'; // A list of headers that are always forwarded to the proxy targets. const safeForwardHeaders = [ @@ -63,12 +63,7 @@ export interface RouterOptions { discovery: DiscoveryService; skipInvalidProxies?: boolean; reviveConsumedRequestBodies?: boolean; -} - -export interface ProxyConfig extends Options { - allowedMethods?: string[]; - allowedHeaders?: string[]; - reviveRequestBody?: boolean; + additionalEndpoints?: ProxyConfig; } // Creates a proxy middleware, possibly with defaults added on top of the @@ -87,7 +82,7 @@ export function buildMiddleware( fullConfig = { target: config }; credentialsPolicy = 'require'; } else { - const { credentials, ...rest } = config as any; + const { credentials, ...rest } = config; fullConfig = rest; credentialsPolicy = credentials ?? 'require'; } @@ -315,7 +310,11 @@ export async function createRouterInternal( const externalUrl = await options.discovery.getExternalBaseUrl('proxy'); const { pathname: pathPrefix } = new URL(externalUrl); - const proxyConfig = readProxyConfig(options.config, options.logger); + const proxyConfig: ProxyConfig = { + ...(options.additionalEndpoints ?? {}), + ...readProxyConfig(options.config, options.logger), + }; + configureMiddlewares( proxyOptions, currentRouter, @@ -358,10 +357,10 @@ function configureMiddlewares( }, router: express.Router, pathPrefix: string, - proxyConfig: any, + proxyConfig: ProxyConfig, httpRouterService?: HttpRouterService, ) { - Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => { + Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => { try { router.use( route, diff --git a/plugins/proxy-node/.eslintrc.js b/plugins/proxy-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/proxy-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/proxy-node/README.md b/plugins/proxy-node/README.md new file mode 100644 index 0000000000..7f48a17cf6 --- /dev/null +++ b/plugins/proxy-node/README.md @@ -0,0 +1,5 @@ +# backstage-plugin-proxy-node-node + +Welcome to the Node.js library package for the proxy-node plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/proxy-node/catalog-info.yaml b/plugins/proxy-node/catalog-info.yaml new file mode 100644 index 0000000000..94dad42af2 --- /dev/null +++ b/plugins/proxy-node/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-proxy-node + title: '@backstage/plugin-proxy-node' + description: The plugin-proxy-node module for @backstage/plugin-proxy-backend +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/plugins/proxy-node/package.json b/plugins/proxy-node/package.json new file mode 100644 index 0000000000..152fea3069 --- /dev/null +++ b/plugins/proxy-node/package.json @@ -0,0 +1,60 @@ +{ + "name": "@backstage/plugin-proxy-node", + "version": "0.0.0", + "description": "The plugin-proxy-node module for @backstage/plugin-proxy-backend", + "backstage": { + "role": "node-library", + "pluginId": "proxy-backend", + "pluginPackages": [ + "@backstage/plugin-proxy-node" + ] + }, + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/proxy-node" + }, + "license": "Apache-2.0", + "exports": { + "./alpha": "./src/alpha/index.ts", + "./package.json": "./package.json" + }, + "main": "src/alpha/index.ts", + "types": "src/alpha/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha/index.ts" + ], + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "http-proxy-middleware": "^2.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/config": "workspace:^", + "@types/http-proxy-middleware": "^1.0.0" + } +} diff --git a/plugins/proxy-node/report-alpha.api.md b/plugins/proxy-node/report-alpha.api.md new file mode 100644 index 0000000000..3608ea7ec6 --- /dev/null +++ b/plugins/proxy-node/report-alpha.api.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-proxy-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { Options } from 'http-proxy-middleware'; + +// @alpha +export interface ProxyConfig extends Options { + // (undocumented) + allowedHeaders?: string[]; + // (undocumented) + allowedMethods?: string[]; + // (undocumented) + credentials?: 'require' | 'forward' | 'dangerously-allow-unauthenticated'; + // (undocumented) + reviveRequestBody?: boolean; +} + +// @alpha +export interface ProxyEndpointsExtensionPoint { + // (undocumented) + addProxyEndpoints(endpoints: Record): void; +} + +// @alpha +export const proxyEndpointsExtensionPoint: ExtensionPoint; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/proxy-node/src/alpha/index.ts b/plugins/proxy-node/src/alpha/index.ts new file mode 100644 index 0000000000..da5eb85f80 --- /dev/null +++ b/plugins/proxy-node/src/alpha/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 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 { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { Options } from 'http-proxy-middleware'; + +/** + * http-proxy-middleware proxy config interface. + * + * @alpha + */ +export interface ProxyConfig extends Options { + allowedMethods?: string[]; + allowedHeaders?: string[]; + reviveRequestBody?: boolean; + credentials?: 'require' | 'forward' | 'dangerously-allow-unauthenticated'; +} + +/** + * Extension point interface for managing proxy endpoints. + * + * @alpha + */ +export interface ProxyEndpointsExtensionPoint { + addProxyEndpoints(endpoints: Record): void; +} + +/** + * Extension point for managing proxy endpoints. + * + * @alpha + */ +export const proxyEndpointsExtensionPoint = + createExtensionPoint({ + id: 'proxy.endpoints', + }); diff --git a/plugins/scaffolder-backend-module-gitlab/report.api.md b/plugins/scaffolder-backend-module-gitlab/report.api.md index 896c0a5899..5b96b142d2 100644 --- a/plugins/scaffolder-backend-module-gitlab/report.api.md +++ b/plugins/scaffolder-backend-module-gitlab/report.api.md @@ -14,7 +14,13 @@ export const createGitlabGroupEnsureExistsAction: (options: { integrations: ScmIntegrationRegistry; }) => TemplateAction< { - path: string[]; + path: ( + | string + | { + name: string; + slug: string; + } + )[]; repoUrl: string; token?: string | undefined; }, @@ -202,6 +208,7 @@ export const createPublishGitlabMergeRequestAction: (options: { projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; + reviewers?: string[] | undefined; }, JsonObject >; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts index 202b397d19..eaeb65ae9c 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.test.ts @@ -196,4 +196,51 @@ describe('gitlab:group:ensureExists', () => { expect(mockContext.output).toHaveBeenCalledWith('groupId', 42); }); + + it(`Should ${examples[4].description}`, async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'group1', + }, + { + id: 2, + full_path: 'group1/group2', + }, + ]); + mockGitlabClient.Groups.create.mockResolvedValue({ + id: 3, + full_path: 'group1/group2/group3', + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabGroupEnsureExistsAction({ integrations }); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[4].example).steps[0].input, + }); + + expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith( + 'Group 3', + 'group3', + { + parentId: 2, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts index 41dd822e82..bbe7ad4722 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.examples.ts @@ -82,4 +82,24 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a group nested within another group using objects', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabGroup', + name: 'Group', + action: 'gitlab:group:ensureExists', + input: { + repoUrl: 'gitlab.com', + path: [ + { name: 'Group 1', slug: 'group1' }, + { name: 'Group 2', slug: 'group2' }, + { name: 'Group 3', slug: 'group3' }, + ], + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts index 1192052e6d..f9dc12eb54 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.test.ts @@ -62,7 +62,7 @@ describe('gitlab:group:ensureExists', () => { const mockContext = createMockActionContext(); - it('should create a new group if it does not exists', async () => { + it('should create a new group from string if it does not exists', async () => { mockGitlabClient.Groups.search.mockResolvedValue([ { id: 1, @@ -94,6 +94,45 @@ describe('gitlab:group:ensureExists', () => { expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); }); + it('should create a new group from object if it does not exists', async () => { + mockGitlabClient.Groups.search.mockResolvedValue([ + { + id: 1, + full_path: 'bar', + }, + { + id: 2, + full_path: 'foo', + }, + ]); + + mockGitlabClient.Groups.create.mockResolvedValue({ + id: 3, + full_path: 'foo/bar', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + path: [ + { name: 'Foo', slug: 'foo' }, + { name: 'Bar is a nice name', slug: 'bar' }, + ], + }, + }); + + expect(mockGitlabClient.Groups.create).toHaveBeenCalledWith( + 'Bar is a nice name', + 'bar', + { + parentId: 2, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('groupId', 3); + }); + it('should return existing group if it does exists', async () => { mockGitlabClient.Groups.search.mockResolvedValue([ { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts index 7134726e6f..50ce7a761b 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabGroupEnsureExists.ts @@ -41,9 +41,18 @@ export const createGitlabGroupEnsureExistsAction = (options: { input: commonGitlabConfig.merge( z.object({ path: z - .array(z.string(), { - description: 'A path of group names that is ensured to exist', - }) + .array( + z.string().or( + z.object({ + name: z.string(), + slug: z.string(), + }), + ), + { + description: + 'A path of group names or objects (name and slug) that is ensured to exist', + }, + ) .min(1), }), ), @@ -68,9 +77,11 @@ export const createGitlabGroupEnsureExistsAction = (options: { let currentPath: string | null = null; let parentId: number | null = null; for (const pathElement of path) { - const fullPath: string = currentPath - ? `${currentPath}/${pathElement}` - : pathElement; + const slug = + typeof pathElement === 'string' ? pathElement : pathElement.slug; + const name = + typeof pathElement === 'string' ? pathElement : pathElement.name; + const fullPath: string = currentPath ? `${currentPath}/${slug}` : slug; const result = (await api.Groups.search( fullPath, )) as unknown as Array; // recast since the return type for search is wrong in the gitbeaker typings @@ -81,8 +92,8 @@ export const createGitlabGroupEnsureExistsAction = (options: { ctx.logger.info(`creating missing group ${fullPath}`); parentId = ( await api.Groups.create( - pathElement, - pathElement, + name, + slug, parentId ? { parentId: parentId, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.examples.test.ts index 087d474807..e6aea81285 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.examples.test.ts @@ -42,6 +42,58 @@ const mockGitlabClient = { default_branch: 'main', }; }), + show: jest.fn(async (_: any) => { + return { + default_branch: 'main', + }; + }), + edit: jest.fn(async (_: any) => { + return { + default_branch: 'main', + }; + }), + }, + MergeRequestApprovals: { + allApprovalRules: jest.fn(async (_: any) => { + return [ + { + id: 123, + name: 'rule1', + rule_type: 'regular', + eligible_approvers: [ + { + id: 123, + username: 'John Smith', + }, + { + id: 456, + username: 'Jane Doe', + }, + ], + approvals_required: 1, + users: [], + contains_hidden_groups: false, + report_type: null, + section: null, + source_rule: { approvals_required: 1 }, + overridden: false, + }, + { + id: 456, + name: 'All Members', + rule_type: 'any_approver', + eligible_approvers: [], + approvals_required: 1, + users: [], + groups: [], + contains_hidden_groups: false, + report_type: null, + section: null, + source_rule: { approvals_required: 1 }, + overridden: false, + }, + ]; + }), }, Projects: { create: jest.fn(), diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts index b5db1b4811..874764b5ba 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts @@ -51,11 +51,86 @@ const mockGitlabClient = { create: jest.fn(), }, MergeRequests: { - create: jest.fn(async (_: any) => { + create: jest.fn(async (repoId: string) => { + if (repoId === 'owner/repo-without-approvals') { + return { + iid: 5, + }; + } + return { + default_branch: 'main', + iid: 4, + }; + }), + show: jest.fn(async (repoId: string, iid: number) => { + if (repoId === 'owner/repo' && iid === 4) { + return { + iid: 4, + }; + } else if (repoId === 'owner/repo-without-approvals' && iid === 5) { + return { + iid: 5, + }; + } return { default_branch: 'main', }; }), + edit: jest.fn(async (_: any) => { + return { + default_branch: 'main', + }; + }), + }, + MergeRequestApprovals: { + allApprovalRules: jest.fn( + async (repoId: string, options: { mergerequestIId: number }) => { + if ( + repoId === 'owner/repo-without-approvals' && + options.mergerequestIId === 5 + ) { + return []; + } + return [ + { + id: 123, + name: 'rule1', + rule_type: 'regular', + eligible_approvers: [ + { + id: 234, + username: 'Bob Vance', + }, + { + id: 345, + username: 'Dina Fox', + }, + ], + approvals_required: 1, + users: [], + contains_hidden_groups: false, + report_type: null, + section: null, + source_rule: { approvals_required: 1 }, + overridden: false, + }, + { + id: 456, + name: 'All Members', + rule_type: 'any_approver', + eligible_approvers: [], + approvals_required: 1, + users: [], + groups: [], + contains_hidden_groups: false, + report_type: null, + section: null, + source_rule: { approvals_required: 1 }, + overridden: false, + }, + ]; + }, + ), }, Projects: { create: jest.fn(), @@ -67,13 +142,28 @@ const mockGitlabClient = { }, Users: { all: jest.fn(async (userOptions: { username: string }) => { - if (userOptions.username !== 'John Smith') - throw new Error('user does not exist'); - return [ - { - id: 123, - }, - ]; + switch (userOptions.username) { + case 'John Smith': + return [ + { + id: 123, + }, + ]; + case 'Bob Vance': + return [ + { + id: 234, + }, + ]; + case 'Jane Doe': + return [ + { + id: 456, + }, + ]; + default: + throw new Error('user does not exist'); + } }), }, Repositories: { @@ -517,6 +607,236 @@ describe('createGitLabMergeRequest', () => { }); }); + describe('createGitlabMergeRequestWithReviewers', () => { + it('no reviewers are set when a no reviewer are passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'This is an important change', + removeSourceBranch: false, + targetPath: 'Subdirectory', + assignee: 'John Smith', + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'This is an important change', + removeSourceBranch: false, + assigneeId: 123, + }, + ); + expect( + mockGitlabClient.MergeRequestApprovals.allApprovalRules, + ).toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.edit).toHaveBeenCalledWith( + 'owner/repo', + 4, + { + reviewerIds: [234, 345], // Approval Rule Members + }, + ); + }); + + it('reviewer is set correcly when a valid reviewer username is passed in options in combination with MR approval rules', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'This is an important change', + removeSourceBranch: false, + targetPath: 'Subdirectory', + assignee: 'John Smith', + reviewers: ['Jane Doe', 'Bob Vance'], + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'This is an important change', + removeSourceBranch: false, + assigneeId: 123, + reviewerIds: [456, 234], // Jane Doe and Bob Vance + }, + ); + expect( + mockGitlabClient.MergeRequestApprovals.allApprovalRules, + ).toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.edit).toHaveBeenCalledWith( + 'owner/repo', + 4, + { + reviewerIds: [234, 345, 456], // Approval Rule Members + Jane Doe (individual reviewer) but no duplicates (Bob Vance) + }, + ); + }); + + it('reviewer is set correcly when a valid reviewer username is passed in options and no MR rules exist', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo-without-approvals&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'This is an important change', + removeSourceBranch: false, + targetPath: 'Subdirectory', + assignee: 'John Smith', + reviewers: ['Jane Doe', 'Bob Vance'], + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo-without-approvals', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo-without-approvals', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'This is an important change', + removeSourceBranch: false, + assigneeId: 123, + reviewerIds: [456, 234], // Jane Doe and Bob Vance + }, + ); + expect( + mockGitlabClient.MergeRequestApprovals.allApprovalRules, + ).toHaveBeenCalledWith('owner/repo-without-approvals', { + mergerequestIId: 5, + }); + expect(mockGitlabClient.MergeRequests.edit).not.toHaveBeenCalled(); + }); + + it('reviewers are set correcly when valid reviewers username are passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'This is an important change', + removeSourceBranch: false, + targetPath: 'Subdirectory', + assignee: 'John Smith', + reviewers: ['Jane Doe', 'John Smith'], + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'This is an important change', + removeSourceBranch: false, + assigneeId: 123, + reviewerIds: [456, 123], + }, + ); + }); + + it('assignee is not set when a valid assignee username is not passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'This is an important change', + removeSourceBranch: false, + targetPath: 'Subdirectory', + reviewers: ['John Doe'], + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Branches.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + ); + expect(mockGitlabClient.Commits.create).not.toHaveBeenCalled(); + expect(mockGitlabClient.MergeRequests.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'main', + 'Create my new MR', + { + description: 'This is an important change', + removeSourceBranch: false, + assigneeId: undefined, + reviewerIds: [], + }, + ); + }); + }); + describe('createGitLabMergeRequestWithoutCommitAction', () => { it('default commitAction is auto', async () => { const input = { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts index 4e4812fdcf..607a904d0a 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts @@ -20,7 +20,12 @@ import { SerializedFile, serializeDirectoryContents, } from '@backstage/plugin-scaffolder-node'; -import { Gitlab, RepositoryTreeSchema, CommitAction } from '@gitbeaker/rest'; +import { + Gitlab, + RepositoryTreeSchema, + CommitAction, + SimpleUserSchema, +} from '@gitbeaker/rest'; import path from 'path'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { InputError } from '@backstage/errors'; @@ -100,6 +105,7 @@ export const createPublishGitlabMergeRequestAction = (options: { projectid?: string; removeSourceBranch?: boolean; assignee?: string; + reviewers?: string[]; }>({ id: 'publish:gitlab:merge-request', examples, @@ -179,6 +185,14 @@ which uses additional API calls in order to detect whether to 'create', 'update' type: 'string', description: 'User this merge request will be assigned to', }, + reviewers: { + title: 'Merge Request Reviewers', + type: 'array', + items: { + type: 'string', + }, + description: 'Users that will be assigned as reviewers', + }, }, }, output: { @@ -207,6 +221,7 @@ which uses additional API calls in order to detect whether to 'create', 'update' async handler(ctx) { const { assignee, + reviewers, branchName, targetBranchName, description, @@ -242,6 +257,27 @@ which uses additional API calls in order to detect whether to 'create', 'update' } } + let reviewerIds: number[] | undefined = undefined; // Explicitly set to undefined. Strangely, passing an empty array to the API will result the other options being undefined also being explicity passed to the Gitlab API call (e.g. assigneeId) + if (reviewers !== undefined) { + reviewerIds = ( + await Promise.all( + reviewers.map(async reviewer => { + try { + const reviewerUser = await api.Users.all({ + username: reviewer, + }); + return reviewerUser[0].id; + } catch (e) { + ctx.logger.warn( + `Failed to find gitlab user id for ${reviewer}: ${e}. Proceeding with MR creation without reviewer.`, + ); + return undefined; + } + }), + ) + ).filter(id => id !== undefined) as number[]; + } + let fileRoot: string; if (sourcePath) { fileRoot = resolveSafeChildPath(ctx.workspacePath, sourcePath); @@ -355,7 +391,7 @@ which uses additional API calls in order to detect whether to 'create', 'update' } } try { - const mergeRequestUrl = await api.MergeRequests.create( + let mergeRequest = await api.MergeRequests.create( repoID, branchName, String(targetBranch), @@ -364,12 +400,59 @@ which uses additional API calls in order to detect whether to 'create', 'update' description, removeSourceBranch: removeSourceBranch ? removeSourceBranch : false, assigneeId, + reviewerIds, }, - ).then(mergeRequest => mergeRequest.web_url ?? mergeRequest.webUrl); + ); + + // Because we don't know the code owners before the MR is created, we can't check the approval rules beforehand. + // Getting the approval rules beforehand is very difficult, especially, because of the inheritance rules for groups. + // Code owners take a moment to be processed and added to the approval rules after the MR is created. + + while ( + mergeRequest.detailed_merge_status === 'preparing' || + mergeRequest.detailed_merge_status === 'approvals_syncing' || + mergeRequest.detailed_merge_status === 'checking' + ) { + mergeRequest = await api.MergeRequests.show(repoID, mergeRequest.iid); + ctx.logger.info(`${mergeRequest.detailed_merge_status}`); + } + + const approvalRules = await api.MergeRequestApprovals.allApprovalRules( + repoID, + { + mergerequestIId: mergeRequest.iid, + }, + ); + + if (approvalRules.length !== 0) { + const eligibleApprovers = approvalRules + .filter(rule => rule.eligible_approvers !== undefined) + .map(rule => { + return rule.eligible_approvers as SimpleUserSchema[]; + }) + .flat(); + + const eligibleUserIds = new Set([ + ...eligibleApprovers.map(user => user.id), + ...(reviewerIds ?? []), + ]); + + mergeRequest = await api.MergeRequests.edit( + repoID, + mergeRequest.iid, + { + reviewerIds: Array.from(eligibleUserIds), + }, + ); + } + ctx.output('projectid', repoID); ctx.output('targetBranchName', targetBranch); ctx.output('projectPath', repoID); - ctx.output('mergeRequestUrl', mergeRequestUrl); + ctx.output( + 'mergeRequestUrl', + mergeRequest.web_url ?? mergeRequest.webUrl, + ); } catch (e) { throw new InputError( `Merge request creation failed. ${getErrorMessage(e)}`, diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts new file mode 100644 index 0000000000..869bda30ec --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.test.ts @@ -0,0 +1,89 @@ +/* + * 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 { ScmIntegrations } from '@backstage/integration'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createGitlabProjectMigrateAction } from './gitlabProjectMigrate'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { examples } from './gitlabProjectMigrate.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + Migrations: { + create: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:group:migrate', () => { + let instance: TemplateAction; + + beforeEach(() => { + jest.clearAllMocks(); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.local.com', + token: 'token', + apiBaseUrl: 'https://api.gitlab.local.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + instance = createGitlabProjectMigrateAction({ integrations }); + }); + + describe('Migrate a gitlab project to a different gitlab instance in another', () => { + const workspacePath = createMockDirectory().resolve('workspace'); + + it(`Execute example ${examples[0].description}`, async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Migrations.create).toHaveBeenCalledWith( + { + url: 'https://gitlab.remote.com', + access_token: 'lolstoken', + }, + [ + { + sourceType: 'project_entity', + sourceFullPath: 'my/wonderful-repo', + destinationSlug: 'precious-repo', + destinationNamespace: 'my/local/site', + }, + ], + ); + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.ts new file mode 100644 index 0000000000..9fe53cb415 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.examples.ts @@ -0,0 +1,40 @@ +/* + * 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: 'Import repo from remote GitLab instance', + example: yaml.stringify({ + steps: [ + { + id: 'importGitLabRepo', + action: 'gitlab:repo:push', + name: 'Push changes to gitlab repository', + input: { + destinationAccessToken: 'tokenslols', + destinationUrl: + 'gitlab.local.com?repo=precious-repo&owner=my%2Flocal%2Fsite', + sourceAccessToken: 'lolstoken', + sourceFullPath: 'my/wonderful-repo', + sourceUrl: 'https://gitlab.remote.com', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts new file mode 100644 index 0000000000..8e3bc6560d --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { ScmIntegrations } from '@backstage/integration'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createGitlabProjectMigrateAction } from './gitlabProjectMigrate'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +const mockGitlabClient = { + Migrations: { + create: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('createGitlabRepoMigrate', () => { + let instance: TemplateAction; + + beforeEach(() => { + jest.clearAllMocks(); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'token', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + instance = createGitlabProjectMigrateAction({ integrations }); + }); + + describe('createGitlabRepoImportAction', () => { + const workspacePath = createMockDirectory().resolve('workspace'); + + it('default repo import action is created', async () => { + const input = { + destinationAccessToken: 'moreLOLsToken', + destinationUrl: + 'gitlab.com?owner=migrated%2Ffoo%2Fbar&repo=migrated-go-lang', + sourceUrl: 'https://gitlab.remote.com', + sourceAccessToken: 'lolstoken', + sourceFullPath: 'foo/bar/go-lang', + }; + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Migrations.create).toHaveBeenCalledWith( + { + url: 'https://gitlab.remote.com', + access_token: 'lolstoken', + }, + [ + { + sourceType: 'project_entity', + sourceFullPath: 'foo/bar/go-lang', + destinationSlug: 'migrated-go-lang', + destinationNamespace: 'migrated/foo/bar', + }, + ], + ); + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts new file mode 100644 index 0000000000..659215c1c0 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectMigrate.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2024 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 { + createTemplateAction, + parseRepoUrl, +} from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { InputError } from '@backstage/errors'; +import { createGitlabApi } from './helpers'; +import { examples } from './gitlabRepoPush.examples'; +import { MigrationEntityOptions } from '@gitbeaker/rest'; + +/** + * Create a new action that imports a gitlab project into another gitlab project (potentially from another gitlab instance). + * + * @public + */ +export const createGitlabProjectMigrateAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + + return createTemplateAction<{ + destinationAccessToken: string; + destinationUrl: string; + sourceAccessToken: string; + sourceFullPath: string; + sourceUrl: string; + }>({ + id: 'gitlab:group:migrate', + examples, + schema: { + input: { + required: [ + 'destinationAccessToken', + 'destinationUrl', + 'sourceAccessToken', + 'sourceFullPath', + 'sourceUrl', + ], + type: 'object', + properties: { + destinationAccessToken: { + type: 'string', + title: 'Target Repository Access Token', + description: `The token to use for authorization to the target GitLab'`, + }, + destinationUrl: { + type: 'string', + title: 'Target Project Location', + description: `Accepts the format 'gitlab.com?repo=project_name&owner=group_name' where 'project_name' is the repository name and 'group_name' is a group or username`, + }, + sourceAccessToken: { + type: 'string', + title: 'Source Group Access Token', + description: `The token to use for authorization to the source GitLab'`, + }, + sourceFullPath: { + type: 'string', + title: 'Group Full Path', + description: + 'Full path to the project in the source Gitlab instance', + }, + sourceUrl: { + type: 'string', + title: 'Source URL Location', + description: `Accepts the format 'https://gitlab.com/'`, + }, + }, + }, + output: { + type: 'object', + properties: { + importedRepoUrl: { + title: 'URL to the newly imported repo', + type: 'string', + }, + }, + }, + }, + + async handler(ctx) { + const { + destinationAccessToken, + destinationUrl, + sourceAccessToken, + sourceFullPath, + sourceUrl, + } = ctx.input; + + const { + host: destinationHost, + repo: destinationSlug, + owner: destinationNamespace, + } = parseRepoUrl(destinationUrl, integrations); + + if (!destinationNamespace) { + throw new InputError( + `Failed to determine target repository to migrate to. Make sure destinationUrl matches the format 'gitlab.myorg.com?repo=project_name&owner=group_name'`, + ); + } + + const api = createGitlabApi({ + integrations, + token: destinationAccessToken, + repoUrl: destinationUrl, + }); + + const migrationEntity: MigrationEntityOptions[] = [ + { + sourceType: 'project_entity', + sourceFullPath: sourceFullPath, + destinationSlug: destinationSlug, + destinationNamespace: destinationNamespace, + }, + ]; + + const sourceConfig = { + url: sourceUrl, + access_token: sourceAccessToken, + }; + + try { + await api.Migrations.create(sourceConfig, migrationEntity); + } catch (e: any) { + throw new InputError( + `Failed to transfer repo ${sourceFullPath}. Make sure that ${sourceFullPath} exists in ${sourceUrl}, and token has enough rights.\nError: ${e}`, + ); + } + + ctx.output( + 'importedRepoUrl', + `${destinationHost}/${destinationNamespace}/${destinationSlug}`, + ); + }, + }); +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/module.ts b/plugins/scaffolder-backend-module-gitlab/src/module.ts index 9f18532cdf..7dbfddfc1b 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/module.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/module.ts @@ -34,6 +34,7 @@ import { createTriggerGitlabPipelineAction, editGitlabIssueAction, } from './actions'; +import { createGitlabProjectMigrateAction } from './actions/gitlabProjectMigrate'; import { createHandleAutocompleteRequest } from './autocomplete/autocomplete'; /** @@ -55,6 +56,7 @@ export const gitlabModule = createBackendModule({ scaffolder.addActions( createGitlabGroupEnsureExistsAction({ integrations }), + createGitlabProjectMigrateAction({ integrations }), createGitlabIssueAction({ integrations }), createGitlabProjectAccessTokenAction({ integrations }), createGitlabProjectDeployTokenAction({ integrations }), diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 20273697d3..d2bb6f1bb1 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -358,6 +358,7 @@ export const createPublishGitlabMergeRequestAction: (options: { projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; + reviewers?: string[] | undefined; }, JsonObject >; diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 4f9f86d1ac..4cddb48602 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -132,6 +132,30 @@ export const Form: ( props: PropsWithChildren, ) => React_2.JSX.Element; +// @alpha +export const FormDecoratorBlueprint: ExtensionBlueprint<{ + kind: 'scaffolder-form-decorator'; + name: undefined; + params: { + decorator: ScaffolderFormDecorator; + }; + output: ConfigurableExtensionDataRef< + ScaffolderFormDecorator, + 'scaffolder.form-decorator-loader', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + formDecoratorLoader: ConfigurableExtensionDataRef< + ScaffolderFormDecorator, + 'scaffolder.form-decorator-loader', + {} + >; + }; +}>; + // @alpha export const FormFieldBlueprint: ExtensionBlueprint<{ kind: 'scaffolder-form-field'; diff --git a/plugins/scaffolder-react/src/next/blueprints/FormDecoratorBlueprint.tsx b/plugins/scaffolder-react/src/next/blueprints/FormDecoratorBlueprint.tsx new file mode 100644 index 0000000000..436b1df85c --- /dev/null +++ b/plugins/scaffolder-react/src/next/blueprints/FormDecoratorBlueprint.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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 { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; +import { ScaffolderFormDecorator } from '../extensions'; + +const formDecoratorExtensionDataRef = + createExtensionDataRef().with({ + id: 'scaffolder.form-decorator-loader', + }); + +/** + * @alpha + * Creates extensions that are Field Extensions for the Scaffolder + * */ +export const FormDecoratorBlueprint = createExtensionBlueprint({ + kind: 'scaffolder-form-decorator', + attachTo: { id: 'api:scaffolder/form-decorators', input: 'formDecorators' }, + dataRefs: { + formDecoratorLoader: formDecoratorExtensionDataRef, + }, + output: [formDecoratorExtensionDataRef], + *factory(params: { decorator: ScaffolderFormDecorator }) { + yield formDecoratorExtensionDataRef(params.decorator); + }, +}); diff --git a/plugins/scaffolder-react/src/next/blueprints/index.ts b/plugins/scaffolder-react/src/next/blueprints/index.ts index 165c583ac8..d9d0d2e0b3 100644 --- a/plugins/scaffolder-react/src/next/blueprints/index.ts +++ b/plugins/scaffolder-react/src/next/blueprints/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export * from './FormDecoratorBlueprint'; export * from './FormFieldBlueprint'; export * from './types'; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index e272b57655..7623350d96 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -80,6 +80,33 @@ const _default: FrontendPlugin< factory: AnyApiFactory; }; }>; + 'api:scaffolder/form-decorators': ExtensionDefinition<{ + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + inputs: { + formDecorators: ExtensionInput< + ConfigurableExtensionDataRef< + ScaffolderFormDecorator, + 'scaffolder.form-decorator-loader', + {} + >, + { + singleton: false; + optional: false; + } + >; + }; + kind: 'api'; + name: 'form-decorators'; + params: { + factory: AnyApiFactory; + }; + }>; 'page:scaffolder': ExtensionDefinition<{ kind: 'page'; name: undefined; @@ -173,6 +200,31 @@ export class DefaultScaffolderFormDecoratorsApi getFormDecorators(): Promise; } +// @alpha (undocumented) +export const formDecoratorsApi: ExtensionDefinition<{ + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef; + inputs: { + formDecorators: ExtensionInput< + ConfigurableExtensionDataRef< + ScaffolderFormDecorator, + 'scaffolder.form-decorator-loader', + {} + >, + { + singleton: false; + optional: false; + } + >; + }; + kind: 'api'; + name: 'form-decorators'; + params: { + factory: AnyApiFactory; + }; +}>; + // @alpha (undocumented) export const formDecoratorsApiRef: ApiRef; diff --git a/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts b/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts index 13c4b6d1a2..41bf91dba0 100644 --- a/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts +++ b/plugins/scaffolder/src/alpha/api/FormDecoratorsApi.ts @@ -14,8 +14,15 @@ * limitations under the License. */ +import { + ApiBlueprint, + createApiFactory, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; import { ScaffolderFormDecoratorsApi } from './types'; import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; +import { formDecoratorsApiRef } from './ref'; +import { FormDecoratorBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; /** @alpha */ export class DefaultScaffolderFormDecoratorsApi @@ -37,3 +44,29 @@ export class DefaultScaffolderFormDecoratorsApi return this.options.decorators; } } + +/** @alpha */ +export const formDecoratorsApi = ApiBlueprint.makeWithOverrides({ + name: 'form-decorators', + inputs: { + formDecorators: createExtensionInput([ + FormDecoratorBlueprint.dataRefs.formDecoratorLoader, + ]), + }, + factory(originalFactory, { inputs }) { + const formDecorators = inputs.formDecorators.map(e => + e.get(FormDecoratorBlueprint.dataRefs.formDecoratorLoader), + ); + + return originalFactory({ + factory: createApiFactory({ + api: formDecoratorsApiRef, + deps: {}, + factory: () => + DefaultScaffolderFormDecoratorsApi.create({ + decorators: formDecorators, + }), + }), + }); + }, +}); diff --git a/plugins/scaffolder/src/alpha/api/index.ts b/plugins/scaffolder/src/alpha/api/index.ts index 07d4b65a3b..b6161dd252 100644 --- a/plugins/scaffolder/src/alpha/api/index.ts +++ b/plugins/scaffolder/src/alpha/api/index.ts @@ -16,4 +16,4 @@ export { formDecoratorsApiRef } from './ref'; export type { ScaffolderFormDecoratorsApi } from './types'; -export { DefaultScaffolderFormDecoratorsApi } from './FormDecoratorsApi'; +export * from './FormDecoratorsApi'; diff --git a/plugins/scaffolder/src/alpha/plugin.tsx b/plugins/scaffolder/src/alpha/plugin.tsx index 44ec1f5d4c..e281f51b0f 100644 --- a/plugins/scaffolder/src/alpha/plugin.tsx +++ b/plugins/scaffolder/src/alpha/plugin.tsx @@ -33,6 +33,7 @@ import { scaffolderApi, } from './extensions'; import { formFieldsApi } from '@backstage/plugin-scaffolder-react/alpha'; +import { formDecoratorsApi } from './api'; /** @alpha */ export default createFrontendPlugin({ @@ -53,6 +54,7 @@ export default createFrontendPlugin({ scaffolderApi, scaffolderPage, scaffolderNavItem, + formDecoratorsApi, formFieldsApi, repoUrlPickerFormField, ], diff --git a/storybook/yarn.lock b/storybook/yarn.lock index be21a0fead..1a245bebeb 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -2689,15 +2689,15 @@ __metadata: linkType: hard "eslint-plugin-storybook@npm:^0.11.0": - version: 0.11.1 - resolution: "eslint-plugin-storybook@npm:0.11.1" + version: 0.11.2 + resolution: "eslint-plugin-storybook@npm:0.11.2" dependencies: "@storybook/csf": ^0.1.11 "@typescript-eslint/utils": ^8.8.1 ts-dedent: ^2.2.0 peerDependencies: - eslint: ">=6" - checksum: 8b8eb30b598f3c44c2bbf921e318215338f1159c1fba2d2f6cd5bc0b2ec14515655cf1760b5e11355baddabc2ac8446d4dc18ee6df6d2252555b126ff649a421 + eslint: ">=8" + checksum: 04dab47b676db57aa6c2325d4ab66283f5970133a2615cc68a97e42cb4ae1c79317e6db73bcfc24468e146bdff4c9ffe90a368bf2bcb67593954485f058b33ca languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 5ff1c61c2d..31e9ccc1dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1872,7 +1872,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.19.6, @babel/core@npm:^7.24.7, @babel/core@npm:^7.26.0": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.19.6, @babel/core@npm:^7.24.7": version: 7.26.0 resolution: "@babel/core@npm:7.26.0" dependencies: @@ -2956,28 +2956,6 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-self@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-react-jsx-self@npm:7.25.9" - dependencies: - "@babel/helper-plugin-utils": ^7.25.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 41c833cd7f91b1432710f91b1325706e57979b2e8da44e83d86312c78bbe96cd9ef778b4e79e4e17ab25fa32c72b909f2be7f28e876779ede28e27506c41f4ae - languageName: node - linkType: hard - -"@babel/plugin-transform-react-jsx-source@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-react-jsx-source@npm:7.25.9" - dependencies: - "@babel/helper-plugin-utils": ^7.25.9 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: a3e0e5672e344e9d01fb20b504fe29a84918eaa70cec512c4d4b1b035f72803261257343d8e93673365b72c371f35cf34bb0d129720bf178a4c87812c8b9c662 - languageName: node - linkType: hard - "@babel/plugin-transform-react-jsx@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-react-jsx@npm:7.22.5" @@ -4013,7 +3991,6 @@ __metadata: "@types/yarnpkg__lockfile": ^1.1.4 "@typescript-eslint/eslint-plugin": ^8.17.0 "@typescript-eslint/parser": ^8.16.0 - "@vitejs/plugin-react": ^4.3.1 "@yarnpkg/lockfile": ^1.1.0 "@yarnpkg/parsers": ^3.0.0 bfj: ^8.0.0 @@ -4086,9 +4063,6 @@ __metadata: terser-webpack-plugin: ^5.1.3 ts-morph: ^24.0.0 util: ^0.12.3 - vite: ^5.0.0 - vite-plugin-html: ^3.2.2 - vite-plugin-node-polyfills: ^0.22.0 webpack: ^5.94.0 webpack-dev-server: ^5.0.0 yaml: ^2.0.0 @@ -4097,31 +4071,16 @@ __metadata: yn: ^4.0.0 zod: ^3.22.4 peerDependencies: - "@modyfi/vite-plugin-yaml": ^1.1.0 "@rspack/core": ^1.0.10 "@rspack/dev-server": ^1.0.9 "@rspack/plugin-react-refresh": ^1.0.0 - "@vitejs/plugin-react": ^4.0.4 - vite: ^5.0.0 - vite-plugin-html: ^3.2.0 - vite-plugin-node-polyfills: ^0.22.0 peerDependenciesMeta: - "@modyfi/vite-plugin-yaml": - optional: true "@rspack/core": optional: true "@rspack/dev-server": optional: true "@rspack/plugin-react-refresh": 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 @@ -7511,6 +7470,7 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/plugin-proxy-node": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 "@types/http-proxy-middleware": ^1.0.0 @@ -7528,6 +7488,19 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-proxy-node@workspace:^, @backstage/plugin-proxy-node@workspace:plugins/proxy-node": + version: 0.0.0-use.local + resolution: "@backstage/plugin-proxy-node@workspace:plugins/proxy-node" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@types/http-proxy-middleware": ^1.0.0 + http-proxy-middleware: ^2.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder-backend-module-azure@workspace:^, @backstage/plugin-scaffolder-backend-module-azure@workspace:plugins/scaffolder-backend-module-azure": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-azure@workspace:plugins/scaffolder-backend-module-azure" @@ -12174,19 +12147,6 @@ __metadata: languageName: node linkType: hard -"@modyfi/vite-plugin-yaml@npm:^1.1.0": - version: 1.1.0 - resolution: "@modyfi/vite-plugin-yaml@npm:1.1.0" - dependencies: - "@rollup/pluginutils": 5.1.0 - js-yaml: 4.1.0 - tosource: 2.0.0-alpha.3 - peerDependencies: - vite: ^3.2.7 || ^4.0.5 || ^5.0.5 - checksum: 6989cde89323321b714b69d11b9125c8d3483a8c6ace536df313edd653180e85e6013effbc6cb8b89e6f9d6e107a10f7a82fadf6a44e81255b437702d5dd1db0 - languageName: node - linkType: hard - "@motionone/animation@npm:^10.12.0": version: 10.16.3 resolution: "@motionone/animation@npm:10.16.3" @@ -15828,22 +15788,6 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-inject@npm:^5.0.5": - version: 5.0.5 - resolution: "@rollup/plugin-inject@npm:5.0.5" - dependencies: - "@rollup/pluginutils": ^5.0.1 - estree-walker: ^2.0.2 - magic-string: ^0.30.3 - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - checksum: 22cb772fd6f7178308b2ece95cdde5f8615f6257197832166294552a7e4c0d3976dc996cbfa6470af3151d8b86c00091aa93da5f4db6ec563f11b6db29fd1b63 - languageName: node - linkType: hard - "@rollup/plugin-json@npm:^6.0.0": version: 6.1.0 resolution: "@rollup/plugin-json@npm:6.1.0" @@ -15892,7 +15836,17 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:5.1.0, @rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0": +"@rollup/pluginutils@npm:^4.2.1": + version: 4.2.1 + resolution: "@rollup/pluginutils@npm:4.2.1" + dependencies: + estree-walker: ^2.0.1 + picomatch: ^2.2.2 + checksum: 6bc41f22b1a0f1efec3043899e4d3b6b1497b3dea4d94292d8f83b4cf07a1073ecbaedd562a22d11913ff7659f459677b01b09e9598a98936e746780ecc93a12 + languageName: node + linkType: hard + +"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0": version: 5.1.0 resolution: "@rollup/pluginutils@npm:5.1.0" dependencies: @@ -15908,16 +15862,6 @@ __metadata: languageName: node linkType: hard -"@rollup/pluginutils@npm:^4.2.0, @rollup/pluginutils@npm:^4.2.1": - version: 4.2.1 - resolution: "@rollup/pluginutils@npm:4.2.1" - dependencies: - estree-walker: ^2.0.1 - picomatch: ^2.2.2 - checksum: 6bc41f22b1a0f1efec3043899e4d3b6b1497b3dea4d94292d8f83b4cf07a1073ecbaedd562a22d11913ff7659f459677b01b09e9598a98936e746780ecc93a12 - languageName: node - linkType: hard - "@rollup/rollup-android-arm-eabi@npm:4.27.4": version: 4.27.4 resolution: "@rollup/rollup-android-arm-eabi@npm:4.27.4" @@ -19037,7 +18981,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0, @types/babel__core@npm:^7.20.5": +"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.18.0": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -21379,21 +21323,6 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-react@npm:^4.3.1": - version: 4.3.4 - resolution: "@vitejs/plugin-react@npm:4.3.4" - dependencies: - "@babel/core": ^7.26.0 - "@babel/plugin-transform-react-jsx-self": ^7.25.9 - "@babel/plugin-transform-react-jsx-source": ^7.25.9 - "@types/babel__core": ^7.20.5 - react-refresh: ^0.14.2 - peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - checksum: d417f40d9259a1d5193152f7d9fee081d5bf41cbeef9662ae1123ccc1e26aa4b6b04bc82ebb8c4fbfde9516a746fb3af7da19fdd449819c30f0631daaa10a44b - languageName: node - linkType: hard - "@vitest/expect@npm:2.0.5": version: 2.0.5 resolution: "@vitest/expect@npm:2.0.5" @@ -23101,19 +23030,6 @@ __metadata: languageName: node linkType: hard -"assert@npm:^2.0.0": - version: 2.1.0 - resolution: "assert@npm:2.1.0" - dependencies: - call-bind: ^1.0.2 - is-nan: ^1.3.2 - object-is: ^1.1.5 - object.assign: ^4.1.4 - util: ^0.12.5 - checksum: 1ed1cabba9abe55f4109b3f7292b4e4f3cf2953aad8dc148c0b3c3bd676675c31b1abb32ef563b7d5a19d1715bf90d1e5f09fad2a4ee655199468902da80f7c2 - languageName: node - linkType: hard - "assertion-error@npm:^2.0.1": version: 2.0.1 resolution: "assertion-error@npm:2.0.1" @@ -24000,15 +23916,6 @@ __metadata: languageName: node linkType: hard -"browser-resolve@npm:^2.0.0": - version: 2.0.0 - resolution: "browser-resolve@npm:2.0.0" - dependencies: - resolve: ^1.17.0 - checksum: 69225e73b555bd6d2a08fb93c7342cfcf3b5058b975099c52649cd5c3cec84c2066c5385084d190faedfb849684d9dabe10129f0cd401d1883572f2e6650f440 - languageName: node - linkType: hard - "browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" @@ -24177,7 +24084,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.5.0, buffer@npm:^5.7.1": +"buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -24428,7 +24335,7 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": +"call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": version: 1.0.8 resolution: "call-bind@npm:1.0.8" dependencies: @@ -25204,7 +25111,7 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.10, colorette@npm:^2.0.16, colorette@npm:^2.0.20": +"colorette@npm:^2.0.10, colorette@npm:^2.0.20": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 0c016fea2b91b733eb9f4bcdb580018f52c0bc0979443dad930e5037a968237ac53d9beb98e218d2e9235834f8eebce7f8e080422d6194e957454255bde71d3d @@ -25506,13 +25413,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" @@ -25542,7 +25442,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 @@ -25878,7 +25778,7 @@ __metadata: languageName: node linkType: hard -"create-require@npm:^1.1.0, create-require@npm:^1.1.1": +"create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" checksum: a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff @@ -26089,7 +25989,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: @@ -27162,13 +27062,6 @@ __metadata: languageName: node linkType: hard -"domain-browser@npm:^4.22.0": - version: 4.23.0 - resolution: "domain-browser@npm:4.23.0" - checksum: 95b772f5fa88300240694380e06e03868573acdf86ca392a58c78602d6536dca2097ad2469a1500bd23a1329b09992de846e0b66c364cbf5711a7fee3ee5dac9 - languageName: node - linkType: hard - "domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" @@ -27267,14 +27160,7 @@ __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, dotenv@npm:^16.3.1": +"dotenv@npm:^16.3.1": version: 16.4.5 resolution: "dotenv@npm:16.4.5" checksum: 301a12c3d44fd49888b74eb9ccf9f07a1f5df43f489e7fcb89647a2edcd84c42d6bc349dc8df099cd18f07c35c7b04685c1a4f3e6a6a9e6b30f8d48c15b7f49c @@ -27984,7 +27870,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.21.0, esbuild@npm:^0.21.3": +"esbuild@npm:^0.21.0": version: 0.21.5 resolution: "esbuild@npm:0.21.5" dependencies: @@ -28343,15 +28229,15 @@ __metadata: linkType: hard "eslint-plugin-storybook@npm:^0.11.1": - version: 0.11.1 - resolution: "eslint-plugin-storybook@npm:0.11.1" + version: 0.11.2 + resolution: "eslint-plugin-storybook@npm:0.11.2" dependencies: "@storybook/csf": ^0.1.11 "@typescript-eslint/utils": ^8.8.1 ts-dedent: ^2.2.0 peerDependencies: - eslint: ">=6" - checksum: 8b8eb30b598f3c44c2bbf921e318215338f1159c1fba2d2f6cd5bc0b2ec14515655cf1760b5e11355baddabc2ac8446d4dc18ee6df6d2252555b126ff649a421 + eslint: ">=8" + checksum: 04dab47b676db57aa6c2325d4ab66283f5970133a2615cc68a97e42cb4ae1c79317e6db73bcfc24468e146bdff4c9ffe90a368bf2bcb67593954485f058b33ca languageName: node linkType: hard @@ -28774,7 +28660,6 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 - "@modyfi/vite-plugin-yaml": ^1.1.0 "@octokit/rest": ^19.0.3 "@playwright/test": ^1.32.3 "@testing-library/dom": ^10.0.0 @@ -28785,7 +28670,6 @@ __metadata: "@types/react": "*" "@types/react-dom": "*" "@types/zen-observable": ^0.8.0 - "@vitejs/plugin-react": ^4.3.1 axios: ^1.7.7 cross-env: ^7.0.0 history: ^5.0.0 @@ -28795,9 +28679,6 @@ __metadata: react-router: ^6.3.0 react-router-dom: ^6.3.0 react-use: ^17.2.4 - vite: ^5.0.0 - vite-plugin-html: ^3.2.2 - vite-plugin-node-polyfills: ^0.22.0 zen-observable: ^0.10.0 languageName: unknown linkType: soft @@ -29215,7 +29096,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -29976,7 +29857,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:10.1.0, fs-extra@npm:^10.0.0, fs-extra@npm:^10.0.1": +"fs-extra@npm:10.1.0, fs-extra@npm:^10.0.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" dependencies: @@ -30074,7 +29955,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -30093,7 +29974,7 @@ __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": +"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -31292,7 +31173,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: @@ -32422,16 +32303,6 @@ __metadata: languageName: node linkType: hard -"is-nan@npm:^1.3.2": - version: 1.3.2 - resolution: "is-nan@npm:1.3.2" - dependencies: - call-bind: ^1.0.0 - define-properties: ^1.1.3 - checksum: 5dfadcef6ad12d3029d43643d9800adbba21cf3ce2ec849f734b0e14ee8da4070d82b15fdb35138716d02587c6578225b9a22779cab34888a139cc43e4e3610a - languageName: node - linkType: hard - "is-network-error@npm:^1.0.0": version: 1.0.1 resolution: "is-network-error@npm:1.0.1" @@ -32893,13 +32764,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-timers-promises@npm:^1.0.1": - version: 1.0.1 - resolution: "isomorphic-timers-promises@npm:1.0.1" - checksum: 16ef59f0fbcceba1a037c74b5f7195d252ae058724ccd3e53b37ad034e8498f5532084e8ab18e7940ba3fa8fca2f21403d00eed15802ab1f7cab7c099cba62a8 - languageName: node - linkType: hard - "isomorphic-ws@npm:5.0.0, isomorphic-ws@npm:^5.0.0": version: 5.0.0 resolution: "isomorphic-ws@npm:5.0.0" @@ -33646,7 +33510,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:4.1.0, js-yaml@npm:=4.1.0, js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": +"js-yaml@npm:=4.1.0, js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" dependencies: @@ -37359,16 +37223,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" @@ -37478,41 +37332,6 @@ __metadata: languageName: node linkType: hard -"node-stdlib-browser@npm:^1.2.0": - version: 1.2.0 - resolution: "node-stdlib-browser@npm:1.2.0" - dependencies: - assert: ^2.0.0 - browser-resolve: ^2.0.0 - browserify-zlib: ^0.2.0 - buffer: ^5.7.1 - console-browserify: ^1.1.0 - constants-browserify: ^1.0.0 - create-require: ^1.1.1 - crypto-browserify: ^3.11.0 - domain-browser: ^4.22.0 - events: ^3.0.0 - https-browserify: ^1.0.0 - isomorphic-timers-promises: ^1.0.1 - os-browserify: ^0.3.0 - path-browserify: ^1.0.1 - pkg-dir: ^5.0.0 - process: ^0.11.10 - punycode: ^1.4.1 - querystring-es3: ^0.2.1 - readable-stream: ^3.6.0 - stream-browserify: ^3.0.0 - stream-http: ^3.2.0 - string_decoder: ^1.0.0 - timers-browserify: ^2.0.4 - tty-browserify: 0.0.1 - url: ^0.11.0 - util: ^0.12.4 - vm-browserify: ^1.0.1 - checksum: fe491f0839319fd9bb95964c6f7da81fc7fde4c3ac9062aa367f19bc5a6060d0d9e423d3de4196cb51f8259d6aaf6cf380048c48a86eb3721c6223dd0dcc5bfd - languageName: node - linkType: hard - "node-watch@npm:0.7.3": version: 0.7.3 resolution: "node-watch@npm:0.7.3" @@ -37888,16 +37707,6 @@ __metadata: languageName: node linkType: hard -"object-is@npm:^1.1.5": - version: 1.1.6 - resolution: "object-is@npm:1.1.6" - dependencies: - call-bind: ^1.0.7 - define-properties: ^1.2.1 - checksum: 3ea22759967e6f2380a2cbbd0f737b42dc9ddb2dfefdb159a1b927fea57335e1b058b564bfa94417db8ad58cddab33621a035de6f5e5ad56d89f2dd03e66c6a1 - languageName: node - linkType: hard - "object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" @@ -38977,13 +38786,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 - "pathval@npm:^2.0.0": version: 2.0.0 resolution: "pathval@npm:2.0.0" @@ -39309,15 +39111,6 @@ __metadata: languageName: node linkType: hard -"pkg-dir@npm:^5.0.0": - version: 5.0.0 - resolution: "pkg-dir@npm:5.0.0" - dependencies: - find-up: ^5.0.0 - checksum: b167bb8dac7bbf22b1d5e30ec223e6b064b84b63010c9d49384619a36734caf95ed23ad23d4f9bd975e8e8082b60a83395f43a89bb192df53a7c25a38ecb57d9 - languageName: node - linkType: hard - "pkg-up@npm:^3.1.0": version: 3.1.0 resolution: "pkg-up@npm:3.1.0" @@ -39837,7 +39630,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.1.0, postcss@npm:^8.4.33, postcss@npm:^8.4.43": +"postcss@npm:^8.1.0, postcss@npm:^8.4.33": version: 8.4.49 resolution: "postcss@npm:8.4.49" dependencies: @@ -40355,7 +40148,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^1.2.4, punycode@npm:^1.3.2, punycode@npm:^1.4.1": +"punycode@npm:^1.2.4, punycode@npm:^1.3.2": version: 1.4.1 resolution: "punycode@npm:1.4.1" checksum: fa6e698cb53db45e4628559e557ddaf554103d2a96a1d62892c8f4032cd3bc8871796cae9eabc1bc700e2b6677611521ce5bb1d9a27700086039965d0cf34518 @@ -40429,7 +40222,7 @@ __metadata: languageName: node linkType: hard -"querystring-es3@npm:^0.2.0, querystring-es3@npm:^0.2.1": +"querystring-es3@npm:^0.2.0": version: 0.2.1 resolution: "querystring-es3@npm:0.2.1" checksum: 691e8d6b8b157e7cd49ae8e83fcf86de39ab3ba948c25abaa94fba84c0986c641aa2f597770848c64abce290ed17a39c9df6df737dfa7e87c3b63acc7d225d61 @@ -41105,7 +40898,7 @@ __metadata: languageName: node linkType: hard -"react-refresh@npm:^0.14.0, react-refresh@npm:^0.14.2": +"react-refresh@npm:^0.14.0": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" checksum: d80db4bd40a36dab79010dc8aa317a5b931f960c0d83c4f3b81f0552cbcf7f29e115b84bb7908ec6a1eb67720fff7023084eff73ece8a7ddc694882478464382 @@ -42032,7 +41825,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:1.22.8, resolve@npm:^1.1.6, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1, resolve@npm:~1.22.2": +"resolve@npm:1.22.8, resolve@npm:^1.1.6, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1, resolve@npm:~1.22.2": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -42058,7 +41851,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@1.22.8#~builtin, resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin, resolve@patch:resolve@~1.22.1#~builtin, resolve@patch:resolve@~1.22.2#~builtin": +"resolve@patch:resolve@1.22.8#~builtin, resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin, resolve@patch:resolve@~1.22.1#~builtin, resolve@patch:resolve@~1.22.2#~builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -42369,7 +42162,7 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.20.0, rollup@npm:^4.27.3": +"rollup@npm:^4.27.3": version: 4.27.4 resolution: "rollup@npm:4.27.4" dependencies: @@ -43827,7 +43620,7 @@ __metadata: languageName: node linkType: hard -"stream-browserify@npm:3.0.0, stream-browserify@npm:^3.0.0": +"stream-browserify@npm:3.0.0": version: 3.0.0 resolution: "stream-browserify@npm:3.0.0" dependencies: @@ -43883,18 +43676,6 @@ __metadata: languageName: node linkType: hard -"stream-http@npm:^3.2.0": - version: 3.2.0 - resolution: "stream-http@npm:3.2.0" - dependencies: - builtin-status-codes: ^3.0.0 - inherits: ^2.0.4 - readable-stream: ^3.6.0 - xtend: ^4.0.2 - checksum: c9b78453aeb0c84fcc59555518ac62bacab9fa98e323e7b7666e5f9f58af8f3155e34481078509b02929bd1268427f664d186604cdccee95abc446099b339f83 - languageName: node - linkType: hard - "stream-json@npm:^1.7.4": version: 1.8.0 resolution: "stream-json@npm:1.8.0" @@ -45161,7 +44942,7 @@ __metadata: languageName: node linkType: hard -"tosource@npm:2.0.0-alpha.3, tosource@npm:^2.0.0-alpha.3": +"tosource@npm:^2.0.0-alpha.3": version: 2.0.0-alpha.3 resolution: "tosource@npm:2.0.0-alpha.3" checksum: bc03a7571de8ed4306e6721283fa891f2adcab9dd80c46f6f177d4259b34bb192fe3a2cb3e1e2ce16f9db0bc7e534acfcb5478ab094b0ba255f98abfce6dab46 @@ -45501,13 +45282,6 @@ __metadata: languageName: node linkType: hard -"tty-browserify@npm:0.0.1": - version: 0.0.1 - resolution: "tty-browserify@npm:0.0.1" - checksum: 93b745d43fa5a7d2b948fa23be8d313576d1d884b48acd957c07710bac1c0d8ac34c0556ad4c57c73d36e11741763ef66b3fb4fb97b06b7e4d525315a3cd45f5 - languageName: node - linkType: hard - "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -46739,83 +46513,6 @@ __metadata: languageName: node linkType: hard -"vite-plugin-html@npm:^3.2.2": - version: 3.2.2 - resolution: "vite-plugin-html@npm:3.2.2" - 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: 2fd6e1f91f74a4432222ed28e68d5f27e58ccbc9ad44e71ff9d02b684b358b0c634bdb4dd32e9d93d09e88d83c3b7b74b89698e25510bc5b94173cdc067b3ac2 - languageName: node - linkType: hard - -"vite-plugin-node-polyfills@npm:^0.22.0": - version: 0.22.0 - resolution: "vite-plugin-node-polyfills@npm:0.22.0" - dependencies: - "@rollup/plugin-inject": ^5.0.5 - node-stdlib-browser: ^1.2.0 - peerDependencies: - vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - checksum: c08d3df0d5cc3102280483d3f7b216f92a18e0708fcb9f67f78f01ab865474254756bacee17caa90b3f46afe5834cb9d8de0dd0e58c1bbbdae1b949edc1e6b57 - languageName: node - linkType: hard - -"vite@npm:^5.0.0": - version: 5.4.11 - resolution: "vite@npm:5.4.11" - dependencies: - esbuild: ^0.21.3 - fsevents: ~2.3.3 - postcss: ^8.4.43 - rollup: ^4.20.0 - peerDependencies: - "@types/node": ^18.0.0 || >=20.0.0 - less: "*" - lightningcss: ^1.21.0 - sass: "*" - sass-embedded: "*" - stylus: "*" - sugarss: "*" - terser: ^5.4.0 - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - "@types/node": - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - bin: - vite: bin/vite.js - checksum: 8c5b31d17487b69c40a30419dc0ade9f33360eb6893dbfa33a90980271bd74d35ae550b5cbb2a9e640f0df41ea36fd1bb4f222c98f6d02e607080f20832e69e8 - languageName: node - linkType: hard - "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -47650,7 +47347,7 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.0, xtend@npm:^4.0.2": +"xtend@npm:^4.0.0": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a