(
// Get the responsive values for the variant and weight
const responsiveVariant = useResponsiveValue(variant);
const responsiveWeight = useResponsiveValue(weight);
+ const responsiveColor = useResponsiveValue(color);
return (
(
'canon-Text',
responsiveVariant && `canon-Text--variant-${responsiveVariant}`,
responsiveWeight && `canon-Text--weight-${responsiveWeight}`,
+ responsiveColor && `canon-Text--color-${responsiveColor}`,
className,
)}
style={style}
diff --git a/packages/canon/src/components/Text/styles.css b/packages/canon/src/components/Text/styles.css
index 871960b8db..8252554280 100644
--- a/packages/canon/src/components/Text/styles.css
+++ b/packages/canon/src/components/Text/styles.css
@@ -48,3 +48,23 @@
.canon-Text--weight-bold {
font-weight: var(--canon-font-weight-bold);
}
+
+.canon-Text--color-primary {
+ color: var(--canon-fg-primary);
+}
+
+.canon-Text--color-secondary {
+ color: var(--canon-fg-secondary);
+}
+
+.canon-Text--color-danger {
+ color: var(--canon-fg-danger);
+}
+
+.canon-Text--color-warning {
+ color: var(--canon-fg-warning);
+}
+
+.canon-Text--color-success {
+ color: var(--canon-fg-success);
+}
diff --git a/packages/canon/src/components/Text/types.ts b/packages/canon/src/components/Text/types.ts
index 4fdc824120..d4f1615694 100644
--- a/packages/canon/src/components/Text/types.ts
+++ b/packages/canon/src/components/Text/types.ts
@@ -27,6 +27,18 @@ export interface TextProps {
| 'label'
| Partial>;
weight?: 'regular' | 'bold' | Partial>;
+ color?:
+ | 'primary'
+ | 'secondary'
+ | 'danger'
+ | 'warning'
+ | 'success'
+ | Partial<
+ Record<
+ Breakpoint,
+ 'primary' | 'secondary' | 'danger' | 'warning' | 'success'
+ >
+ >;
className?: string;
style?: CSSProperties;
}
diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx
new file mode 100644
index 0000000000..dd5558437b
--- /dev/null
+++ b/packages/canon/src/components/TextField/TextField.stories.tsx
@@ -0,0 +1,163 @@
+/*
+ * 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 React, { useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { TextField } from './TextField';
+import { Form } from '@base-ui-components/react/form';
+import { Button } from '../Button';
+import { Flex } from '../Flex';
+
+const meta = {
+ title: 'Components/TextField',
+ component: TextField,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ name: 'url',
+ placeholder: 'Enter a URL',
+ },
+};
+
+export const Filled: Story = {
+ args: {
+ ...Default.args,
+ defaultValue: 'https://example.com',
+ },
+};
+
+export const WithLabel: Story = {
+ args: {
+ ...Default.args,
+ label: 'Label',
+ },
+};
+
+export const WithDescription: Story = {
+ args: {
+ ...WithLabel.args,
+ description: 'Description',
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ ...WithLabel.args,
+ disabled: true,
+ },
+};
+
+export const Sizes: Story = {
+ args: {
+ ...Default.args,
+ label: 'Label',
+ description: 'Description',
+ },
+ render: args => (
+
+
+
+
+ ),
+};
+
+export const Responsive: Story = {
+ args: {
+ ...WithLabel.args,
+ size: {
+ initial: 'small',
+ sm: 'medium',
+ },
+ },
+};
+
+export const ShowErrorOnSubmit: Story = {
+ args: {
+ ...WithLabel.args,
+ pattern: 'https?://.*',
+ type: 'url',
+ required: true,
+ label: 'Homepage',
+ name: 'url',
+ value: 'https://backstage-fake-site.com',
+ },
+ decorators: [
+ Story => {
+ const [errors, setErrors] = useState | undefined>(
+ undefined,
+ );
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ const formData = new FormData(event.currentTarget);
+
+ setLoading(true);
+
+ await new Promise(resolve => {
+ setTimeout(resolve, 200);
+ });
+
+ try {
+ const url = new URL(formData.get('url') as string);
+
+ const allowedHosts = [
+ 'backstage.io',
+ 'beta.backstage.io',
+ 'www.backstage.io',
+ ];
+
+ if (!allowedHosts.includes(url.hostname)) {
+ setErrors({ url: 'The example domain is not allowed' });
+ setLoading(false);
+
+ return;
+ }
+
+ setErrors(undefined);
+ setLoading(false);
+
+ return;
+ } catch {
+ setErrors({ url: 'This is not a valid URL' });
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+ },
+ ],
+};
diff --git a/packages/canon/src/components/Field/Field.styles.css b/packages/canon/src/components/TextField/TextField.styles.css
similarity index 60%
rename from packages/canon/src/components/Field/Field.styles.css
rename to packages/canon/src/components/TextField/TextField.styles.css
index 6a8e3c8e22..bd36661b23 100644
--- a/packages/canon/src/components/Field/Field.styles.css
+++ b/packages/canon/src/components/TextField/TextField.styles.css
@@ -44,10 +44,47 @@
padding-top: var(--canon-space-1_5);
}
-.canon-FieldValidity {
- font-size: var(--canon-font-size-2);
+.canon-Input {
+ border-radius: var(--canon-radius-3);
+ border: 1px solid var(--canon-border);
+ padding: 0 var(--canon-space-4);
+ background-color: var(--canon-bg-surface-1);
+ font-size: var(--canon-font-size-3);
+ font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
- color: var(--canon-fg-secondary);
- margin: 0;
- padding-top: var(--canon-space-1_5);
+ color: var(--canon-fg-primary);
+ transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
+ width: 100%;
+}
+
+.canon-Input::placeholder {
+ color: var(--canon-fg-secondary);
+}
+
+.canon-Input:hover {
+ border-color: var(--canon-border-hover);
+}
+
+.canon-Input:focus-visible {
+ outline-color: var(--canon-border-pressed);
+ outline-width: 0px;
+ border-color: var(--canon-border-pressed);
+}
+
+.canon-Input[data-invalid] {
+ border-color: var(--canon-fg-danger);
+}
+
+.canon-Input[data-disabled] {
+ opacity: 0.5;
+ cursor: not-allowed;
+ border: 1px solid var(--canon-border-disabled);
+}
+
+.canon-Input--size-small {
+ height: 2rem;
+}
+
+.canon-Input--size-medium {
+ height: 2.5rem;
}
diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx
new file mode 100644
index 0000000000..ccea9e8c65
--- /dev/null
+++ b/packages/canon/src/components/TextField/TextField.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 React, { forwardRef } from 'react';
+import { Field } from '@base-ui-components/react/field';
+import { Input } from '@base-ui-components/react/input';
+import { useResponsiveValue } from '../../hooks/useResponsiveValue';
+import clsx from 'clsx';
+
+import type { TextFieldProps } from './types';
+
+/** @public */
+export const TextField = forwardRef(
+ (props: TextFieldProps, ref) => {
+ const {
+ className,
+ size = 'medium',
+ label,
+ description,
+ name,
+ ...rest
+ } = props;
+
+ // Get the responsive value for the variant
+ const responsiveSize = useResponsiveValue(size);
+
+ return (
+
+ {label && (
+ {label}
+ )}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ );
+ },
+);
+
+TextField.displayName = 'TextField';
diff --git a/plugins/events-backend/src/deprecated.ts b/packages/canon/src/components/TextField/index.ts
similarity index 83%
rename from plugins/events-backend/src/deprecated.ts
rename to packages/canon/src/components/TextField/index.ts
index cce853b2af..29df00ccaf 100644
--- a/plugins/events-backend/src/deprecated.ts
+++ b/packages/canon/src/components/TextField/index.ts
@@ -14,5 +14,5 @@
* limitations under the License.
*/
-export { EventsBackend } from './service/EventsBackend';
-export { DefaultEventBroker } from './service/DefaultEventBroker';
+export * from './TextField';
+export type { TextFieldProps } from './types';
diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts
new file mode 100644
index 0000000000..100d5fcb17
--- /dev/null
+++ b/packages/canon/src/components/TextField/types.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Breakpoint } from '../../types';
+
+/** @public */
+export interface TextFieldProps
+ extends Omit, 'size'> {
+ /**
+ * The class name of the text field
+ */
+ className?: string;
+
+ /**
+ * The size of the text field
+ * @defaultValue 'medium'
+ */
+ size?: 'small' | 'medium' | Partial>;
+
+ /**
+ * The label of the text field
+ */
+ label?: string;
+
+ /**
+ * The description of the text field
+ */
+ description?: string;
+
+ /**
+ * The name of the text field
+ */
+ name: string;
+}
diff --git a/packages/canon/src/components/Tooltip/Tooltip.stories.tsx b/packages/canon/src/components/Tooltip/Tooltip.stories.tsx
new file mode 100644
index 0000000000..4c3168a934
--- /dev/null
+++ b/packages/canon/src/components/Tooltip/Tooltip.stories.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import { Tooltip } from './Tooltip';
+import { Button } from '../Button/Button';
+
+const meta = {
+ title: 'Components/Tooltip',
+ component: Tooltip.Root,
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ children: (
+ <>
+ (
+
+ Button
+
+ )}
+ />
+
+
+ Nice!
+
+
+ >
+ ),
+ },
+};
+
+export const Open: Story = {
+ args: {
+ ...Default.args,
+ open: true,
+ },
+};
+
+export const WithArrow: Story = {
+ args: {
+ open: true,
+ children: (
+ <>
+ (
+
+ Button
+
+ )}
+ />
+
+
+
+
+ Nice!
+
+
+
+ >
+ ),
+ },
+};
diff --git a/packages/canon/src/components/Tooltip/Tooltip.styles.css b/packages/canon/src/components/Tooltip/Tooltip.styles.css
new file mode 100644
index 0000000000..d6850c0d26
--- /dev/null
+++ b/packages/canon/src/components/Tooltip/Tooltip.styles.css
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+.canon-TooltipPopup {
+ box-sizing: border-box;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ display: flex;
+ flex-direction: column;
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.375rem;
+ background-color: canvas;
+ transform-origin: var(--transform-origin);
+ transition: transform 150ms, opacity 150ms;
+ background-color: var(--canon-bg-surface-1);
+ color: var(--canon-fg-primary);
+ outline: 1px solid var(--canon-border);
+ box-shadow: 0 10px 15px -3px var(--canon-border),
+ 0 4px 6px -4px var(--canon-border);
+
+ &[data-starting-style],
+ &[data-ending-style] {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+
+ &[data-instant] {
+ transition-duration: 0ms;
+ }
+}
+
+.canon-TooltipArrow {
+ display: flex;
+
+ &[data-side='top'] {
+ bottom: -8px;
+ rotate: 180deg;
+ }
+
+ &[data-side='bottom'] {
+ top: -8px;
+ rotate: 0deg;
+ }
+
+ &[data-side='left'] {
+ right: -13px;
+ rotate: 90deg;
+ }
+
+ &[data-side='right'] {
+ left: -13px;
+ rotate: -90deg;
+ }
+}
+
+.canon-TooltipArrow-fill {
+ fill: var(--canon-bg-surface-1);
+}
+
+.canon-TooltipArrow-outer-stroke {
+ @media (prefers-color-scheme: light) {
+ fill: var(--canon-border);
+ }
+}
+
+.canon-TooltipArrow-inner-stroke {
+ @media (prefers-color-scheme: dark) {
+ /* fill: var(--canon-border); */
+ }
+}
diff --git a/packages/canon/src/components/Tooltip/Tooltip.tsx b/packages/canon/src/components/Tooltip/Tooltip.tsx
new file mode 100644
index 0000000000..72e2e73b7a
--- /dev/null
+++ b/packages/canon/src/components/Tooltip/Tooltip.tsx
@@ -0,0 +1,93 @@
+/*
+ * 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 React from 'react';
+import { Tooltip as TooltipPrimitive } from '@base-ui-components/react/tooltip';
+import clsx from 'clsx';
+
+const TooltipTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
+
+const TooltipPositioner = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TooltipPositioner.displayName = TooltipPrimitive.Positioner.displayName;
+
+const TooltipPopup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TooltipPopup.displayName = TooltipPrimitive.Popup.displayName;
+
+const TooltipArrow = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+
+));
+TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName;
+
+/** @public */
+export const Tooltip = {
+ Provider: TooltipPrimitive.Provider,
+ Root: TooltipPrimitive.Root,
+ Trigger: TooltipTrigger,
+ Portal: TooltipPrimitive.Portal,
+ Positioner: TooltipPositioner,
+ Popup: TooltipPopup,
+ Arrow: TooltipArrow,
+};
diff --git a/packages/canon/src/components/Field/index.ts b/packages/canon/src/components/Tooltip/index.ts
similarity index 95%
rename from packages/canon/src/components/Field/index.ts
rename to packages/canon/src/components/Tooltip/index.ts
index 297ed9b27b..cf97688854 100644
--- a/packages/canon/src/components/Field/index.ts
+++ b/packages/canon/src/components/Tooltip/index.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export * from './Field';
+export * from './Tooltip';
diff --git a/packages/canon/src/contexts/canon.tsx b/packages/canon/src/contexts/canon.tsx
deleted file mode 100644
index 69a9e30b6f..0000000000
--- a/packages/canon/src/contexts/canon.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 React, { createContext, ReactNode, useContext } from 'react';
-import { icons } from '../components/Icon/icons';
-import { IconMap, IconNames } from '../components/Icon/types';
-
-/** @public */
-export interface CanonContextProps {
- icons: IconMap;
-}
-
-/** @public */
-export interface CanonProviderProps {
- children?: ReactNode;
- overrides?: Partial>;
-}
-
-const CanonContext = createContext({
- icons,
-});
-
-/** @public */
-export const CanonProvider = (props: CanonProviderProps) => {
- const { children, overrides } = props;
-
- // Merge provided overrides with default icons
- const combinedIcons = { ...icons, ...overrides };
-
- return (
-
- {children}
-
- );
-};
-
-/** @public */
-export const useCanon = () => useContext(CanonContext);
diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css
index 1abee6eb5d..2480baab14 100644
--- a/packages/canon/src/css/components.css
+++ b/packages/canon/src/css/components.css
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-/* Components */
@import '../components/Box/styles.css';
@import '../components/Button/styles.css';
@import '../components/Flex/styles.css';
@@ -25,5 +24,10 @@
@import '../components/Table/styles.css';
@import '../components/Text/styles.css';
@import '../components/Heading/styles.css';
-@import '../components/Input/Input.styles.css';
-@import '../components/Field/Field.styles.css';
+@import '../components/IconButton/styles.css';
+@import '../components/TextField/TextField.styles.css';
+@import '../components/Menu/Menu.styles.css';
+@import '../components/Link/styles.css';
+@import '../components/Tooltip/Tooltip.styles.css';
+@import '../components/ScrollArea/ScrollArea.styles.css';
+@import '../components/Select/Select.styles.css';
diff --git a/packages/canon/src/css/core.css b/packages/canon/src/css/core.css
index cc0e5ab85a..d0686e94f7 100644
--- a/packages/canon/src/css/core.css
+++ b/packages/canon/src/css/core.css
@@ -15,11 +15,16 @@
*/
/* Normalize */
-@import './base.css';
+@import './normalize.css';
@import './utilities.css';
-/* Light theme tokens */
+/* Global reset */
+:where(a) {
+ color: inherit;
+ text-decoration: none;
+}
+/* Light theme tokens */
:root {
/* Font families */
--canon-font-regular: system-ui;
@@ -72,7 +77,8 @@
/* Background Colors */
--canon-bg: #f8f8f8;
- --canon-bg-elevated: #fff;
+ --canon-bg-surface-1: #fff;
+ --canon-bg-surface-2: #ececec;
--canon-bg-solid: #1f5493;
--canon-bg-solid-hover: #163a66;
--canon-bg-solid-pressed: #0f2b4e;
@@ -107,13 +113,19 @@
--canon-border-danger: #f87a7a;
--canon-border-warning: #e36d05;
--canon-border-success: #53db83;
+
+ /* Special Colors */
+ --canon-ring: #1f5493;
+ --canon-scrollbar: #a0a0a03b;
+ --canon-scrollbar-thumb: #a0a0a0;
}
/* Dark theme tokens */
[data-theme='dark'] {
/* Background Colors */
--canon-bg: #000000;
- --canon-bg-elevated: #191919;
+ --canon-bg-surface-1: #191919;
+ --canon-bg-surface-2: #242424;
--canon-bg-solid: #9cc9ff;
--canon-bg-solid-hover: #83b9fd;
--canon-bg-solid-pressed: #83b9fd;
@@ -148,4 +160,9 @@
--canon-border-danger: #f87a7a;
--canon-border-warning: #e36d05;
--canon-border-success: #53db83;
+
+ /* Special Colors */
+ --canon-ring: #1f5493;
+ --canon-scrollbar: #3636363a;
+ --canon-scrollbar-thumb: #575757;
}
diff --git a/packages/canon/src/css/styles.css b/packages/canon/src/css/styles.css
new file mode 100644
index 0000000000..f5d623389f
--- /dev/null
+++ b/packages/canon/src/css/styles.css
@@ -0,0 +1,18 @@
+/*
+ * 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 './core.css';
+@import './components.css';
diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts
index 1168990966..7435b6dd60 100644
--- a/packages/canon/src/index.ts
+++ b/packages/canon/src/index.ts
@@ -21,7 +21,7 @@
*/
// Providers
-export * from './contexts/canon';
+export * from './components/Icon/context';
// Layout components
export * from './components/Box';
@@ -34,10 +34,14 @@ export * from './components/Heading';
// UI components
export * from './components/Button';
export * from './components/Icon';
+export * from './components/IconButton';
export * from './components/Checkbox';
export * from './components/Table';
-export * from './components/Input';
-export * from './components/Field';
+export * from './components/TextField';
+export * from './components/Tooltip';
+export * from './components/Menu';
+export * from './components/ScrollArea';
+export * from './components/Select';
// Types
export * from './types';
diff --git a/packages/canon/src/utils/extractProps.ts b/packages/canon/src/utils/extractProps.ts
index 8631f4e228..1668117909 100644
--- a/packages/canon/src/utils/extractProps.ts
+++ b/packages/canon/src/utils/extractProps.ts
@@ -14,6 +14,16 @@
* limitations under the License.
*/
+type BasePropDef = {
+ type: string;
+ values?: readonly unknown[];
+ default?: unknown;
+ required?: boolean;
+ className?: string;
+ responsive?: true;
+ customProperties?: string[];
+};
+
export function extractProps(
props: {
className?: string;
@@ -22,45 +32,53 @@ export function extractProps(
as?: keyof JSX.IntrinsicElements;
[key: string]: any;
},
- propDefs: { [key: string]: any },
+ propDefs: { [name in string]: BasePropDef },
) {
let className: string[] = (props.className || '').split(' ');
let style: React.CSSProperties = { ...props.style };
- const hasProp = (key: string) => props.hasOwnProperty(key);
for (const key in propDefs) {
+ const propDef = propDefs[key];
+
// Check if the prop is present or has a default value
- if (!hasProp(key) && !propDefs[key].hasOwnProperty('default')) {
+ if (!Object.hasOwn(props, key) && !propDef.hasOwnProperty('default')) {
continue; // Skip processing if neither is present
}
- const value = hasProp(key) ? props[key] : propDefs[key].default;
- const propDefsValues = propDefs[key].values;
- const propDefsCustomProperties = propDefs[key].customProperties;
- const propDefsClassName = propDefs[key].className;
- const isResponsive = propDefs[key].responsive;
+ const value = Object.hasOwn(props, key)
+ ? (props[key] as unknown)
+ : propDefs[key].default;
+ const propDefsValues = propDef.values;
+ const propDefsCustomProperties = propDef.customProperties;
+ const propDefsClassName = propDef.className;
+ const isResponsive = propDef.responsive;
- const handleValue = (val: string, prefix: string = '') => {
+ const handleValue = (val: unknown, prefix: string = '') => {
// Skip adding class name if the key is "as"
if (key === 'as') return;
- if (propDefsValues.includes(val)) {
+ if (propDefsValues?.includes(val)) {
className.push(`${prefix}${propDefsClassName}-${val}`);
} else {
- const customPropertyKey =
- isResponsive && prefix
- ? `${propDefsCustomProperties}-${prefix.slice(0, -1)}`
- : propDefsCustomProperties;
- (style as any)[customPropertyKey] = val;
+ if (propDefsCustomProperties) {
+ for (const customProperty of propDefsCustomProperties) {
+ const customPropertyKey =
+ isResponsive && prefix
+ ? `${customProperty}-${prefix.slice(0, -1)}`
+ : customProperty;
+ style[customPropertyKey as keyof typeof style] = val as any;
+ }
+ }
className.push(`${prefix}${propDefsClassName}`);
}
};
- if (isResponsive && typeof value === 'object') {
+ if (isResponsive && typeof value === 'object' && value !== null) {
+ const breakpointValues = value as { [key: string]: unknown };
// Handle responsive object values
- for (const breakpoint in value) {
+ for (const breakpoint in breakpointValues) {
const prefix = breakpoint === 'initial' ? '' : `${breakpoint}:`;
- handleValue(value[breakpoint], prefix);
+ handleValue(breakpointValues[breakpoint], prefix);
}
} else {
handleValue(value);
diff --git a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts
index 1bf554672c..be1fa78117 100644
--- a/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts
+++ b/packages/catalog-client/src/schema/openapi/generated/apis/Api.client.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 The Backstage Authors
+ * Copyright 2025 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.
@@ -22,20 +22,20 @@ import { FetchApi } from '../types/fetch';
import crossFetch from 'cross-fetch';
import { pluginId } from '../pluginId';
import * as parser from 'uri-template';
-import { AnalyzeLocationRequest } from '../models/AnalyzeLocationRequest.model';
-import { AnalyzeLocationResponse } from '../models/AnalyzeLocationResponse.model';
-import { CreateLocation201Response } from '../models/CreateLocation201Response.model';
-import { CreateLocationRequest } from '../models/CreateLocationRequest.model';
import { EntitiesBatchResponse } from '../models/EntitiesBatchResponse.model';
import { EntitiesQueryResponse } from '../models/EntitiesQueryResponse.model';
import { Entity } from '../models/Entity.model';
import { EntityAncestryResponse } from '../models/EntityAncestryResponse.model';
import { EntityFacetsResponse } from '../models/EntityFacetsResponse.model';
import { GetEntitiesByRefsRequest } from '../models/GetEntitiesByRefsRequest.model';
-import { GetLocations200ResponseInner } from '../models/GetLocations200ResponseInner.model';
-import { Location } from '../models/Location.model';
import { RefreshEntityRequest } from '../models/RefreshEntityRequest.model';
import { ValidateEntityRequest } from '../models/ValidateEntityRequest.model';
+import { AnalyzeLocationRequest } from '../models/AnalyzeLocationRequest.model';
+import { AnalyzeLocationResponse } from '../models/AnalyzeLocationResponse.model';
+import { CreateLocation201Response } from '../models/CreateLocation201Response.model';
+import { CreateLocationRequest } from '../models/CreateLocationRequest.model';
+import { GetLocations200ResponseInner } from '../models/GetLocations200ResponseInner.model';
+import { Location } from '../models/Location.model';
/**
* Wraps the Response type to convey a type on the json call.
@@ -54,21 +54,6 @@ export type TypedResponse = Omit & {
export interface RequestOptions {
token?: string;
}
-/**
- * @public
- */
-export type AnalyzeLocation = {
- body: AnalyzeLocationRequest;
-};
-/**
- * @public
- */
-export type CreateLocation = {
- body: CreateLocationRequest;
- query: {
- dryRun?: string;
- };
-};
/**
* @public
*/
@@ -77,14 +62,6 @@ export type DeleteEntityByUid = {
uid: string;
};
};
-/**
- * @public
- */
-export type DeleteLocation = {
- path: {
- id: string;
- };
-};
/**
* @public
*/
@@ -159,6 +136,41 @@ export type GetEntityFacets = {
filter?: Array;
};
};
+/**
+ * @public
+ */
+export type RefreshEntity = {
+ body: RefreshEntityRequest;
+};
+/**
+ * @public
+ */
+export type ValidateEntity = {
+ body: ValidateEntityRequest;
+};
+/**
+ * @public
+ */
+export type AnalyzeLocation = {
+ body: AnalyzeLocationRequest;
+};
+/**
+ * @public
+ */
+export type CreateLocation = {
+ body: CreateLocationRequest;
+ query: {
+ dryRun?: string;
+ };
+};
+/**
+ * @public
+ */
+export type DeleteLocation = {
+ path: {
+ id: string;
+ };
+};
/**
* @public
*/
@@ -181,18 +193,6 @@ export type GetLocationByEntity = {
* @public
*/
export type GetLocations = {};
-/**
- * @public
- */
-export type RefreshEntity = {
- body: RefreshEntityRequest;
-};
-/**
- * @public
- */
-export type ValidateEntity = {
- body: ValidateEntityRequest;
-};
/**
* @public
@@ -209,59 +209,6 @@ export class DefaultApiClient {
this.fetchApi = options.fetchApi || { fetch: crossFetch };
}
- /**
- * Validate a given location.
- * @param analyzeLocationRequest -
- */
- public async analyzeLocation(
- // @ts-ignore
- request: AnalyzeLocation,
- options?: RequestOptions,
- ): Promise> {
- const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
-
- const uriTemplate = `/analyze-location`;
-
- const uri = parser.parse(uriTemplate).expand({});
-
- return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
- headers: {
- 'Content-Type': 'application/json',
- ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
- },
- method: 'POST',
- body: JSON.stringify(request.body),
- });
- }
-
- /**
- * Create a location for a given target.
- * @param createLocationRequest -
- * @param dryRun -
- */
- public async createLocation(
- // @ts-ignore
- request: CreateLocation,
- options?: RequestOptions,
- ): Promise> {
- const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
-
- const uriTemplate = `/locations{?dryRun}`;
-
- const uri = parser.parse(uriTemplate).expand({
- ...request.query,
- });
-
- return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
- headers: {
- 'Content-Type': 'application/json',
- ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
- },
- method: 'POST',
- body: JSON.stringify(request.body),
- });
- }
-
/**
* Delete a single entity by UID.
* @param uid -
@@ -289,40 +236,115 @@ export class DefaultApiClient {
}
/**
- * Delete a location by id.
- * @param id -
- */
- public async deleteLocation(
- // @ts-ignore
- request: DeleteLocation,
- options?: RequestOptions,
- ): Promise> {
- const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
+ * Get all entities matching a given filter.
+ * @param fields - By default the full entities are returned, but you can pass in a `fields` query
+parameter which selects what parts of the entity data to retain. This makes the
+response smaller and faster to transfer, and may allow the catalog to perform
+more efficient queries.
- const uriTemplate = `/locations/{id}`;
+The query parameter value is a comma separated list of simplified JSON paths
+like above. Each path corresponds to the key of either a value, or of a subtree
+root that you want to keep in the output. The rest is pruned away. For example,
+specifying `?fields=metadata.name,metadata.annotations,spec` retains only the
+`name` and `annotations` fields of the `metadata` of each entity (it'll be an
+object with at most two keys), keeps the entire `spec` unchanged, and cuts out
+all other roots such as `relations`.
- const uri = parser.parse(uriTemplate).expand({
- id: request.path.id,
- });
+Some more real world usable examples:
- return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
- headers: {
- 'Content-Type': 'application/json',
- ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
- },
- method: 'DELETE',
- });
+- Return only enough data to form the full ref of each entity:
+
+ `/entities/by-query?fields=kind,metadata.namespace,metadata.name`
+
+ * @param limit - Number of records to return in the response.
+ * @param filter - You can pass in one or more filter sets that get matched against each entity.
+Each filter set is a number of conditions that all have to match for the
+condition to be true (conditions effectively have an AND between them). At least
+one filter set has to be true for the entity to be part of the result set
+(filter sets effectively have an OR between them).
+
+Example:
+
+```text
+/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type
+
+ Return entities that match
+
+ Filter set 1:
+ Condition 1: kind = user
+ AND
+ Condition 2: metadata.namespace = default
+
+ OR
+
+ Filter set 2:
+ Condition 1: kind = group
+ AND
+ Condition 2: spec.type exists
+```
+
+Each condition is either on the form `<key>`, or on the form `<key>=<value>`.
+The first form asserts on the existence of a certain key (with any value), and
+the second asserts that the key exists and has a certain value. All checks are
+always case _insensitive_.
+
+In all cases, the key is a simplified JSON path in a given piece of entity data.
+Each part of the path is a key of an object, and the traversal also descends
+through arrays. There are two special forms:
+
+- Array items that are simple value types (such as strings) match on a key-value
+ pair where the key is the item as a string, and the value is the string `true`
+- Relations can be matched on a `relations.<type>=<targetRef>` form
+
+Let's look at a simplified example to illustrate the concept:
+
+```json
+{
+ "a": {
+ "b": ["c", { "d": 1 }],
+ "e": 7
}
+}
+```
- /**
- * Get all entities matching a given filter.
- * @param fields - Restrict to just these fields in the response.
- * @param limit - Number of records to return in the response.
- * @param filter - Filter for just the entities defined by this filter.
- * @param offset - Number of records to skip in the query page.
- * @param after - Pointer to the previous page of results.
- * @param order -
- */
+This would match any one of the following conditions:
+
+- `a`
+- `a.b`
+- `a.b.c`
+- `a.b.c=true`
+- `a.b.d`
+- `a.b.d=1`
+- `a.e`
+- `a.e=7`
+
+Some more real world usable examples:
+
+- Return all orphaned entities:
+
+ `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true`
+
+- Return all users and groups:
+
+ `/entities/by-query?filter=kind=user&filter=kind=group`
+
+- Return all service components:
+
+ `/entities/by-query?filter=kind=component,spec.type=service`
+
+- Return all entities with the `java` tag:
+
+ `/entities/by-query?filter=metadata.tags.java`
+
+- Return all users who are members of the `ops` group (note that the full
+ [reference](references.md) of the group is used):
+
+ `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops`
+
+ * @param offset - Number of records to skip in the query page.
+ * @param after - Pointer to the previous page of results.
+ * @param order -
+ */
public async getEntities(
// @ts-ignore
request: GetEntities,
@@ -346,16 +368,148 @@ export class DefaultApiClient {
}
/**
- * Search for entities by a given query.
- * @param fields - Restrict to just these fields in the response.
- * @param limit - Number of records to return in the response.
- * @param offset - Number of records to skip in the query page.
- * @param orderField - The fields to sort returned results by.
- * @param cursor - Cursor to a set page of results.
- * @param filter - Filter for just the entities defined by this filter.
- * @param fullTextFilterTerm - Text search term.
- * @param fullTextFilterFields - A comma separated list of fields to sort returned results by.
- */
+ * Search for entities by a given query.
+ * @param fields - By default the full entities are returned, but you can pass in a `fields` query
+parameter which selects what parts of the entity data to retain. This makes the
+response smaller and faster to transfer, and may allow the catalog to perform
+more efficient queries.
+
+The query parameter value is a comma separated list of simplified JSON paths
+like above. Each path corresponds to the key of either a value, or of a subtree
+root that you want to keep in the output. The rest is pruned away. For example,
+specifying `?fields=metadata.name,metadata.annotations,spec` retains only the
+`name` and `annotations` fields of the `metadata` of each entity (it'll be an
+object with at most two keys), keeps the entire `spec` unchanged, and cuts out
+all other roots such as `relations`.
+
+Some more real world usable examples:
+
+- Return only enough data to form the full ref of each entity:
+
+ `/entities/by-query?fields=kind,metadata.namespace,metadata.name`
+
+ * @param limit - Number of records to return in the response.
+ * @param offset - Number of records to skip in the query page.
+ * @param orderField - By default the entities are returned ordered by their internal uid. You can
+customize the `orderField` query parameters to affect that ordering.
+
+For example, to return entities by their name:
+
+`/entities/by-query?orderField=metadata.name,asc`
+
+Each parameter can be followed by `asc` for ascending lexicographical order or
+`desc` for descending (reverse) lexicographical order.
+
+ * @param cursor - You may pass the `cursor` query parameters to perform cursor based pagination
+through the set of entities. The value of `cursor` will be returned in the response, under the `pageInfo` property:
+
+```json
+ "pageInfo": {
+ "nextCursor": "a-cursor",
+ "prevCursor": "another-cursor"
+ }
+```
+
+If `nextCursor` exists, it can be used to retrieve the next batch of entities. Following the same approach,
+if `prevCursor` exists, it can be used to retrieve the previous batch of entities.
+
+- [`filter`](#filtering), for selecting only a subset of all entities
+- [`fields`](#field-selection), for selecting only parts of the full data
+ structure of each entity
+- `limit` for limiting the number of entities returned (20 is the default)
+- [`orderField`](#ordering), for deciding the order of the entities
+- `fullTextFilter`
+ **NOTE**: [`filter`, `orderField`, `fullTextFilter`] and `cursor` are mutually exclusive. This means that,
+ it isn't possible to change any of [`filter`, `orderField`, `fullTextFilter`] when passing `cursor` as query parameters,
+ as changing any of these properties will affect pagination. If any of `filter`, `orderField`, `fullTextFilter` is specified together with `cursor`, only the latter is taken into consideration.
+
+ * @param filter - You can pass in one or more filter sets that get matched against each entity.
+Each filter set is a number of conditions that all have to match for the
+condition to be true (conditions effectively have an AND between them). At least
+one filter set has to be true for the entity to be part of the result set
+(filter sets effectively have an OR between them).
+
+Example:
+
+```text
+/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type
+
+ Return entities that match
+
+ Filter set 1:
+ Condition 1: kind = user
+ AND
+ Condition 2: metadata.namespace = default
+
+ OR
+
+ Filter set 2:
+ Condition 1: kind = group
+ AND
+ Condition 2: spec.type exists
+```
+
+Each condition is either on the form `<key>`, or on the form `<key>=<value>`.
+The first form asserts on the existence of a certain key (with any value), and
+the second asserts that the key exists and has a certain value. All checks are
+always case _insensitive_.
+
+In all cases, the key is a simplified JSON path in a given piece of entity data.
+Each part of the path is a key of an object, and the traversal also descends
+through arrays. There are two special forms:
+
+- Array items that are simple value types (such as strings) match on a key-value
+ pair where the key is the item as a string, and the value is the string `true`
+- Relations can be matched on a `relations.<type>=<targetRef>` form
+
+Let's look at a simplified example to illustrate the concept:
+
+```json
+{
+ "a": {
+ "b": ["c", { "d": 1 }],
+ "e": 7
+ }
+}
+```
+
+This would match any one of the following conditions:
+
+- `a`
+- `a.b`
+- `a.b.c`
+- `a.b.c=true`
+- `a.b.d`
+- `a.b.d=1`
+- `a.e`
+- `a.e=7`
+
+Some more real world usable examples:
+
+- Return all orphaned entities:
+
+ `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true`
+
+- Return all users and groups:
+
+ `/entities/by-query?filter=kind=user&filter=kind=group`
+
+- Return all service components:
+
+ `/entities/by-query?filter=kind=component,spec.type=service`
+
+- Return all entities with the `java` tag:
+
+ `/entities/by-query?filter=metadata.tags.java`
+
+- Return all users who are members of the `ops` group (note that the full
+ [reference](references.md) of the group is used):
+
+ `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops`
+
+ * @param fullTextFilterTerm - Text search term.
+ * @param fullTextFilterFields - A comma separated list of fields to sort returned results by.
+ */
public async getEntitiesByQuery(
// @ts-ignore
request: GetEntitiesByQuery,
@@ -379,10 +533,93 @@ export class DefaultApiClient {
}
/**
- * Get a batch set of entities given an array of entityRefs.
- * @param filter - Filter for just the entities defined by this filter.
- * @param getEntitiesByRefsRequest -
- */
+ * Get a batch set of entities given an array of entityRefs.
+ * @param filter - You can pass in one or more filter sets that get matched against each entity.
+Each filter set is a number of conditions that all have to match for the
+condition to be true (conditions effectively have an AND between them). At least
+one filter set has to be true for the entity to be part of the result set
+(filter sets effectively have an OR between them).
+
+Example:
+
+```text
+/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type
+
+ Return entities that match
+
+ Filter set 1:
+ Condition 1: kind = user
+ AND
+ Condition 2: metadata.namespace = default
+
+ OR
+
+ Filter set 2:
+ Condition 1: kind = group
+ AND
+ Condition 2: spec.type exists
+```
+
+Each condition is either on the form `<key>`, or on the form `<key>=<value>`.
+The first form asserts on the existence of a certain key (with any value), and
+the second asserts that the key exists and has a certain value. All checks are
+always case _insensitive_.
+
+In all cases, the key is a simplified JSON path in a given piece of entity data.
+Each part of the path is a key of an object, and the traversal also descends
+through arrays. There are two special forms:
+
+- Array items that are simple value types (such as strings) match on a key-value
+ pair where the key is the item as a string, and the value is the string `true`
+- Relations can be matched on a `relations.<type>=<targetRef>` form
+
+Let's look at a simplified example to illustrate the concept:
+
+```json
+{
+ "a": {
+ "b": ["c", { "d": 1 }],
+ "e": 7
+ }
+}
+```
+
+This would match any one of the following conditions:
+
+- `a`
+- `a.b`
+- `a.b.c`
+- `a.b.c=true`
+- `a.b.d`
+- `a.b.d=1`
+- `a.e`
+- `a.e=7`
+
+Some more real world usable examples:
+
+- Return all orphaned entities:
+
+ `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true`
+
+- Return all users and groups:
+
+ `/entities/by-query?filter=kind=user&filter=kind=group`
+
+- Return all service components:
+
+ `/entities/by-query?filter=kind=component,spec.type=service`
+
+- Return all entities with the `java` tag:
+
+ `/entities/by-query?filter=metadata.tags.java`
+
+- Return all users who are members of the `ops` group (note that the full
+ [reference](references.md) of the group is used):
+
+ `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops`
+
+ * @param getEntitiesByRefsRequest -
+ */
public async getEntitiesByRefs(
// @ts-ignore
request: GetEntitiesByRefs,
@@ -493,10 +730,93 @@ export class DefaultApiClient {
}
/**
- * Get all entity facets that match the given filters.
- * @param facet -
- * @param filter - Filter for just the entities defined by this filter.
- */
+ * Get all entity facets that match the given filters.
+ * @param facet -
+ * @param filter - You can pass in one or more filter sets that get matched against each entity.
+Each filter set is a number of conditions that all have to match for the
+condition to be true (conditions effectively have an AND between them). At least
+one filter set has to be true for the entity to be part of the result set
+(filter sets effectively have an OR between them).
+
+Example:
+
+```text
+/entities/by-query?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type
+
+ Return entities that match
+
+ Filter set 1:
+ Condition 1: kind = user
+ AND
+ Condition 2: metadata.namespace = default
+
+ OR
+
+ Filter set 2:
+ Condition 1: kind = group
+ AND
+ Condition 2: spec.type exists
+```
+
+Each condition is either on the form `<key>`, or on the form `<key>=<value>`.
+The first form asserts on the existence of a certain key (with any value), and
+the second asserts that the key exists and has a certain value. All checks are
+always case _insensitive_.
+
+In all cases, the key is a simplified JSON path in a given piece of entity data.
+Each part of the path is a key of an object, and the traversal also descends
+through arrays. There are two special forms:
+
+- Array items that are simple value types (such as strings) match on a key-value
+ pair where the key is the item as a string, and the value is the string `true`
+- Relations can be matched on a `relations.<type>=<targetRef>` form
+
+Let's look at a simplified example to illustrate the concept:
+
+```json
+{
+ "a": {
+ "b": ["c", { "d": 1 }],
+ "e": 7
+ }
+}
+```
+
+This would match any one of the following conditions:
+
+- `a`
+- `a.b`
+- `a.b.c`
+- `a.b.c=true`
+- `a.b.d`
+- `a.b.d=1`
+- `a.e`
+- `a.e=7`
+
+Some more real world usable examples:
+
+- Return all orphaned entities:
+
+ `/entities/by-query?filter=metadata.annotations.backstage.io/orphan=true`
+
+- Return all users and groups:
+
+ `/entities/by-query?filter=kind=user&filter=kind=group`
+
+- Return all service components:
+
+ `/entities/by-query?filter=kind=component,spec.type=service`
+
+- Return all entities with the `java` tag:
+
+ `/entities/by-query?filter=metadata.tags.java`
+
+- Return all users who are members of the `ops` group (note that the full
+ [reference](references.md) of the group is used):
+
+ `/entities/by-query?filter=kind=user,relations.memberof=group:default/ops`
+
+ */
public async getEntityFacets(
// @ts-ignore
request: GetEntityFacets,
@@ -519,6 +839,135 @@ export class DefaultApiClient {
});
}
+ /**
+ * Refresh the entity related to entityRef.
+ * @param refreshEntityRequest -
+ */
+ public async refreshEntity(
+ // @ts-ignore
+ request: RefreshEntity,
+ options?: RequestOptions,
+ ): Promise> {
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
+
+ const uriTemplate = `/refresh`;
+
+ const uri = parser.parse(uriTemplate).expand({});
+
+ return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
+ },
+ method: 'POST',
+ body: JSON.stringify(request.body),
+ });
+ }
+
+ /**
+ * Validate that a passed in entity has no errors in schema.
+ * @param validateEntityRequest -
+ */
+ public async validateEntity(
+ // @ts-ignore
+ request: ValidateEntity,
+ options?: RequestOptions,
+ ): Promise> {
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
+
+ const uriTemplate = `/validate-entity`;
+
+ const uri = parser.parse(uriTemplate).expand({});
+
+ return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
+ },
+ method: 'POST',
+ body: JSON.stringify(request.body),
+ });
+ }
+
+ /**
+ * Validate a given location.
+ * @param analyzeLocationRequest -
+ */
+ public async analyzeLocation(
+ // @ts-ignore
+ request: AnalyzeLocation,
+ options?: RequestOptions,
+ ): Promise> {
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
+
+ const uriTemplate = `/analyze-location`;
+
+ const uri = parser.parse(uriTemplate).expand({});
+
+ return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
+ },
+ method: 'POST',
+ body: JSON.stringify(request.body),
+ });
+ }
+
+ /**
+ * Create a location for a given target.
+ * @param createLocationRequest -
+ * @param dryRun -
+ */
+ public async createLocation(
+ // @ts-ignore
+ request: CreateLocation,
+ options?: RequestOptions,
+ ): Promise> {
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
+
+ const uriTemplate = `/locations{?dryRun}`;
+
+ const uri = parser.parse(uriTemplate).expand({
+ ...request.query,
+ });
+
+ return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
+ },
+ method: 'POST',
+ body: JSON.stringify(request.body),
+ });
+ }
+
+ /**
+ * Delete a location by id.
+ * @param id -
+ */
+ public async deleteLocation(
+ // @ts-ignore
+ request: DeleteLocation,
+ options?: RequestOptions,
+ ): Promise> {
+ const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
+
+ const uriTemplate = `/locations/{id}`;
+
+ const uri = parser.parse(uriTemplate).expand({
+ id: request.path.id,
+ });
+
+ return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
+ },
+ method: 'DELETE',
+ });
+ }
+
/**
* Get a location by id.
* @param id -
@@ -597,54 +1046,4 @@ export class DefaultApiClient {
method: 'GET',
});
}
-
- /**
- * Refresh the entity related to entityRef.
- * @param refreshEntityRequest -
- */
- public async refreshEntity(
- // @ts-ignore
- request: RefreshEntity,
- options?: RequestOptions,
- ): Promise> {
- const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
-
- const uriTemplate = `/refresh`;
-
- const uri = parser.parse(uriTemplate).expand({});
-
- return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
- headers: {
- 'Content-Type': 'application/json',
- ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
- },
- method: 'POST',
- body: JSON.stringify(request.body),
- });
- }
-
- /**
- * Validate that a passed in entity has no errors in schema.
- * @param validateEntityRequest -
- */
- public async validateEntity(
- // @ts-ignore
- request: ValidateEntity,
- options?: RequestOptions,
- ): Promise> {
- const baseUrl = await this.discoveryApi.getBaseUrl(pluginId);
-
- const uriTemplate = `/validate-entity`;
-
- const uri = parser.parse(uriTemplate).expand({});
-
- return await this.fetchApi.fetch(`${baseUrl}${uri}`, {
- headers: {
- 'Content-Type': 'application/json',
- ...(options?.token && { Authorization: `Bearer ${options?.token}` }),
- },
- method: 'POST',
- body: JSON.stringify(request.body),
- });
- }
}
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index 9a0e25af50..688cb7d461 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,119 @@
# @backstage/cli
+## 0.32.0-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/integration@1.16.3-next.0
+ - @backstage/catalog-model@1.7.3
+ - @backstage/cli-common@0.1.15
+ - @backstage/cli-node@0.2.13
+ - @backstage/config@1.3.2
+ - @backstage/config-loader@1.10.0
+ - @backstage/errors@1.2.7
+ - @backstage/eslint-plugin@0.1.10
+ - @backstage/release-manifests@0.0.12
+ - @backstage/types@1.2.1
+
+## 0.32.0-next.0
+
+### Minor Changes
+
+- c7254ae: Internal update to move the `clean`, `pre/postpack` and `fix` commands into their own separate module.
+
+### Patch Changes
+
+- 4ea76f7: Bump @module-federation/enhanced ^0.9.0 to fix GHSA-593f-38f6-jp5m
+- 87a5cb4: Fixed an issue causing the `repo lint` command to fail when the `--max-warnings` option was used.
+- d83f3f4: Resolved a problem where the `start` command did not correctly handle multiple `--require` flags, ensuring all specified modules are now properly loaded.
+- Updated dependencies
+ - @backstage/catalog-model@1.7.3
+ - @backstage/cli-common@0.1.15
+ - @backstage/cli-node@0.2.13
+ - @backstage/config@1.3.2
+ - @backstage/config-loader@1.10.0
+ - @backstage/errors@1.2.7
+ - @backstage/eslint-plugin@0.1.10
+ - @backstage/integration@1.16.2
+ - @backstage/release-manifests@0.0.12
+ - @backstage/types@1.2.1
+
+## 0.31.0
+
+### Minor Changes
+
+- 5b70679: **BREAKING**: ESLint warnings no longer trigger system exit codes like errors do.
+
+ Set the max number of warnings to `-1` during linting to enable the gradual adoption of new ESLint rules. To restore the previous behavior, include the `--max-warnings 0` flag in the `backstage-cli lint` command.
+
+### Patch Changes
+
+- 0586d4c: Internal change to move the `migrate` and `version:*` commands into a new migrate module.
+- e0b226b: build(deps): bump `esbuild` from 0.24.2 to 0.25.0
+- 4d45498: Fixed the package prepack command so that it no longer produces unnecessary `index` entries in the `typesVersions` map, which could cause `/index` to be added when automatically adding imports.
+- 485b3ba: Internal update to move `test` commands to a separate module.
+- a76c482: Internal change to migrate `lint` to the new module system.
+- 8df78bf: Internal update to move build commands to a CLI module.
+- d0fc357: Internal update to move `info` commands to a separate module.
+- f8bd342: Fix a bug in the translation of the deprecated `--scope` option for the `new` command that could cause plugins to have `backstage-backstage-plugin` in their name.
+- Updated dependencies
+ - @backstage/config-loader@1.10.0
+ - @backstage/integration@1.16.2
+ - @backstage/catalog-model@1.7.3
+ - @backstage/cli-common@0.1.15
+ - @backstage/cli-node@0.2.13
+ - @backstage/config@1.3.2
+ - @backstage/errors@1.2.7
+ - @backstage/eslint-plugin@0.1.10
+ - @backstage/release-manifests@0.0.12
+ - @backstage/types@1.2.1
+
+## 0.31.0-next.1
+
+### Minor Changes
+
+- 5b70679: **BREAKING**: ESLint warnings no longer trigger system exit codes like errors do.
+
+ Set the max number of warnings to `-1` during linting to enable the gradual adoption of new ESLint rules. To restore the previous behavior, include the `--max-warnings 0` flag in the `backstage-cli lint` command.
+
+### Patch Changes
+
+- e0b226b: build(deps): bump `esbuild` from 0.24.2 to 0.25.0
+- 4d45498: Fixed the package prepack command so that it no longer produces unnecessary `index` entries in the `typesVersions` map, which could cause `/index` to be added when automatically adding imports.
+- f8bd342: Fix a bug in the translation of the deprecated `--scope` option for the `new` command that could cause plugins to have `backstage-backstage-plugin` in their name.
+- Updated dependencies
+ - @backstage/config-loader@1.10.0-next.0
+ - @backstage/integration@1.16.2-next.0
+ - @backstage/catalog-model@1.7.3
+ - @backstage/cli-common@0.1.15
+ - @backstage/cli-node@0.2.13
+ - @backstage/config@1.3.2
+ - @backstage/errors@1.2.7
+ - @backstage/eslint-plugin@0.1.10
+ - @backstage/release-manifests@0.0.12
+ - @backstage/types@1.2.1
+
+## 0.30.1-next.0
+
+### Patch Changes
+
+- 0586d4c: Internal change to move the `migrate` and `version:*` commands into a new migrate module.
+- 485b3ba: Internal update to move `test` commands to a separate module.
+- 8df78bf: Internal update to move build commands to a CLI module.
+- d0fc357: Internal update to move `info` commands to a separate module.
+- Updated dependencies
+ - @backstage/catalog-model@1.7.3
+ - @backstage/cli-common@0.1.15
+ - @backstage/cli-node@0.2.13
+ - @backstage/config@1.3.2
+ - @backstage/config-loader@1.9.6
+ - @backstage/errors@1.2.7
+ - @backstage/eslint-plugin@0.1.10
+ - @backstage/integration@1.16.1
+ - @backstage/release-manifests@0.0.12
+ - @backstage/types@1.2.1
+
## 0.30.0
### Minor Changes
diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md
index 41b1ddab73..699161966c 100644
--- a/packages/cli/cli-report.md
+++ b/packages/cli/cli-report.md
@@ -19,12 +19,12 @@ Commands:
config:schema [options]
repo [command]
package [command]
- migrate [command]
versions:bump [options]
versions:migrate [options]
+ migrate [command]
build-workspace [options] [packages...]
- create-github-app
info
+ create-github-app
help [command]
```
@@ -199,11 +199,11 @@ Options:
Commands:
start [options]
build [options]
- lint [options] [directories...]
test
clean
prepack
postpack
+ lint [options] [directories...]
help [command]
```
@@ -272,7 +272,7 @@ Options:
--check
--inspect [host]
--inspect-brk [host]
- --require
+ --require
--link
-h, --help
```
@@ -401,11 +401,11 @@ Options:
Commands:
build [options]
+ test [options]
lint [options]
fix [options]
clean
list-deprecations [options]
- test [options]
help [command]
```
@@ -452,6 +452,7 @@ Options:
--since [
--successCache
--successCacheDir ]
+ --max-warnings
--fix
-h, --help
```
diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js
index 3ae455c4b8..8f50e184ff 100644
--- a/packages/cli/config/eslint-factory.js
+++ b/packages/cli/config/eslint-factory.js
@@ -95,7 +95,7 @@ function createConfig(dir, extraConfig = {}) {
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/no-unused-vars': [
- 'warn',
+ 'error',
{
vars: 'all',
args: 'after-used',
@@ -173,7 +173,7 @@ function createConfig(dir, extraConfig = {}) {
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
- 'warn',
+ 'error',
{
vars: 'all',
varsIgnorePattern: '^_',
diff --git a/packages/cli/knip-report.md b/packages/cli/knip-report.md
index bfb5537db1..4ef7b79b0f 100644
--- a/packages/cli/knip-report.md
+++ b/packages/cli/knip-report.md
@@ -1,6 +1,6 @@
# Knip report
-## Unused dependencies (26)
+## Unused dependencies (27)
| Name | Location | Severity |
| :--------------------------- | :----------- | :------- |
@@ -16,6 +16,7 @@
| terser-webpack-plugin | package.json | error |
| eslint-plugin-react | package.json | error |
| @octokit/oauth-app | package.json | error |
+| @types/webpack-env | package.json | error |
| @svgr/plugin-svgo | package.json | error |
| @octokit/graphql | package.json | error |
| @svgr/plugin-jsx | package.json | error |
@@ -60,10 +61,10 @@
## Unlisted dependencies (4)
-| Name | Location | Severity |
-| :-------- | :----------------------------------- | :------- |
-| react-dom | src/lib/bundler/hasReactDomClient.ts | error |
-| react-dom | src/lib/bundler/config.ts | error |
-| react | src/lib/bundler/config.ts | error |
-| react | src/lib/bundler/server.ts | error |
+| Name | Location | Severity |
+| :-------- | :------------------------------------------------- | :------- |
+| react-dom | src/modules/build/lib/bundler/hasReactDomClient.ts | error |
+| react-dom | src/modules/build/lib/bundler/config.ts | error |
+| react | src/modules/build/lib/bundler/config.ts | error |
+| react | src/modules/build/lib/bundler/server.ts | error |
diff --git a/packages/cli/package.json b/packages/cli/package.json
index e59176e13d..8f3f1764c6 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/cli",
- "version": "0.30.0",
+ "version": "0.32.0-next.1",
"description": "CLI for developing Backstage plugins and apps",
"backstage": {
"role": "cli"
@@ -58,7 +58,7 @@
"@backstage/release-manifests": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
- "@module-federation/enhanced": "^0.8.0",
+ "@module-federation/enhanced": "^0.9.0",
"@octokit/graphql": "^5.0.0",
"@octokit/graphql-schema": "^13.7.0",
"@octokit/oauth-app": "^4.2.0",
@@ -95,7 +95,7 @@
"cross-spawn": "^7.0.3",
"css-loader": "^6.5.1",
"ctrlc-windows": "^2.1.0",
- "esbuild": "^0.24.0",
+ "esbuild": "^0.25.0",
"esbuild-loader": "^4.0.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^9.0.0",
diff --git a/packages/cli/src/alpha.ts b/packages/cli/src/alpha.ts
index f6cd2157ae..49e739bbdc 100644
--- a/packages/cli/src/alpha.ts
+++ b/packages/cli/src/alpha.ts
@@ -24,6 +24,11 @@ import chalk from 'chalk';
),
);
const initializer = new CliInitializer();
+ initializer.add(import('./modules/info/alpha'));
initializer.add(import('./modules/config/alpha'));
+ initializer.add(import('./modules/build/alpha'));
+ initializer.add(import('./modules/migrate/alpha'));
+ initializer.add(import('./modules/test/alpha'));
+ initializer.add(import('./modules/lint/alpha'));
await initializer.run();
})();
diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts
index 47c362988c..99f2bc99ef 100644
--- a/packages/cli/src/commands/index.ts
+++ b/packages/cli/src/commands/index.ts
@@ -14,109 +14,41 @@
* limitations under the License.
*/
-import { Command, Option } from 'commander';
+import { Command } from 'commander';
import { lazy } from '../lib/lazy';
import {
configOption,
registerCommands as registerConfigCommands,
} from '../modules/config';
+import {
+ registerPackageCommands as registerPackageBuildCommands,
+ registerRepoCommands as registerRepoBuildCommands,
+ registerCommands as registerBuildCommands,
+} from '../modules/build';
+import { registerCommands as registerInfoCommands } from '../modules/info';
+import { registerCommands as registerMigrateCommand } from '../modules/migrate';
+import {
+ registerRepoCommands as registerRepoTestCommands,
+ registerPackageCommands as registerPackageTestCommands,
+} from '../modules/test';
+import {
+ registerPackageCommands as registerPackageLintCommands,
+ registerRepoCommands as registerRepoLintCommands,
+} from '../modules/lint';
+import {
+ registerPackageCommands as registerMaintenancePackageCommands,
+ registerRepoCommands as registerMaintenanceRepoCommands,
+} from '../modules/maintenance';
export function registerRepoCommand(program: Command) {
const command = program
.command('repo [command]')
.description('Command that run across an entire Backstage project');
- command
- .command('build')
- .description(
- 'Build packages in the project, excluding bundled app and backend packages.',
- )
- .option(
- '--all',
- 'Build all packages, including bundled app and backend packages.',
- )
- .option(
- '--since [',
- 'Only build packages and their dev dependents that changed since the specified ref',
- )
- .option(
- '--minify',
- 'Minify the generated code. Does not apply to app package (app is minified by default).',
- )
- .action(lazy(() => import('./repo/build'), 'command'));
-
- command
- .command('lint')
- .description('Lint all packages in the project')
- .option(
- '--format ]',
- 'Lint report output format',
- 'eslint-formatter-friendly',
- )
- .option(
- '--output-file ',
- 'Write the lint report to a file instead of stdout',
- )
- .option(
- '--since [',
- 'Only lint packages that changed since the specified ref',
- )
- .option(
- '--successCache',
- 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
- )
- .option(
- '--successCacheDir ]',
- 'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
- )
- .option('--fix', 'Attempt to automatically fix violations')
- .action(lazy(() => import('./repo/lint'), 'command'));
-
- command
- .command('fix')
- .description('Automatically fix packages in the project')
- .option(
- '--publish',
- 'Enable additional fixes that only apply when publishing packages',
- )
- .option(
- '--check',
- 'Fail if any packages would have been changed by the command',
- )
- .action(lazy(() => import('./repo/fix'), 'command'));
-
- command
- .command('clean')
- .description('Delete cache and output directories')
- .action(lazy(() => import('./repo/clean'), 'command'));
-
- command
- .command('list-deprecations')
- .description('List deprecations')
- .option('--json', 'Output as JSON')
- .action(lazy(() => import('./repo/list-deprecations'), 'command'));
-
- command
- .command('test')
- .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
- .option(
- '--since [',
- 'Only test packages that changed since the specified ref',
- )
- .option(
- '--successCache',
- 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
- )
- .option(
- '--successCacheDir ]',
- 'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
- )
- .option(
- '--jest-help',
- 'Show help for Jest CLI options, which are passed through',
- )
- .description('Run tests, forwarding args to Jest, defaulting to watch mode')
- .action(lazy(() => import('./repo/test'), 'command'));
+ registerRepoBuildCommands(command);
+ registerRepoTestCommands(command);
+ registerRepoLintCommands(command);
+ registerMaintenanceRepoCommands(command);
}
export function registerScriptCommand(program: Command) {
@@ -135,109 +67,17 @@ export function registerScriptCommand(program: Command) {
'--inspect-brk [host]',
'Enable debugger in Node.js environments, breaking before code starts',
)
- .option('--require ', 'Add a --require argument to the node process')
+ .option(
+ '--require ',
+ 'Add a --require argument to the node process',
+ )
.option('--link ', 'Link an external workspace for module resolution')
.action(lazy(() => import('./start'), 'command'));
- command
- .command('build')
- .description('Build a package for production deployment or publishing')
- .option('--role ', 'Run the command with an explicit package role')
- .option(
- '--minify',
- 'Minify the generated code. Does not apply to app package (app is minified by default).',
- )
- .option(
- '--skip-build-dependencies',
- 'Skip the automatic building of local dependencies. Applies to backend packages only.',
- )
- .option(
- '--stats',
- 'If bundle stats are available, write them to the output directory. Applies to app packages only.',
- )
- .option(
- '--config ',
- 'Config files to load instead of app-config.yaml. Applies to app packages only.',
- (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
- Array(),
- )
- .action(lazy(() => import('./build'), 'command'));
-
- command
- .command('lint [directories...]')
- .option(
- '--format ',
- 'Lint report output format',
- 'eslint-formatter-friendly',
- )
- .option(
- '--output-file ',
- 'Write the lint report to a file instead of stdout',
- )
- .option('--fix', 'Attempt to automatically fix violations')
- .option(
- '--max-warnings ',
- 'Fail if more than this number of warnings. -1 allows warnings. (default: 0)',
- )
- .description('Lint a package')
- .action(lazy(() => import('./lint'), 'default'));
-
- command
- .command('test')
- .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args
- .helpOption(', --backstage-cli-help') // Let Jest handle help
- .description('Run tests, forwarding args to Jest, defaulting to watch mode')
- .action(lazy(() => import('./test'), 'default'));
-
- command
- .command('clean')
- .description('Delete cache directories')
- .action(lazy(() => import('./clean/clean'), 'default'));
-
- command
- .command('prepack')
- .description('Prepares a package for packaging before publishing')
- .action(lazy(() => import('./pack'), 'pre'));
-
- command
- .command('postpack')
- .description('Restores the changes made by the prepack command')
- .action(lazy(() => import('./pack'), 'post'));
-}
-
-export function registerMigrateCommand(program: Command) {
- const command = program
- .command('migrate [command]')
- .description('Migration utilities');
-
- command
- .command('package-roles')
- .description(`Add package role field to packages that don't have it`)
- .action(lazy(() => import('./migrate/packageRole'), 'default'));
-
- command
- .command('package-scripts')
- .description('Set package scripts according to each package role')
- .action(lazy(() => import('./migrate/packageScripts'), 'command'));
-
- command
- .command('package-exports')
- .description('Synchronize package subpath export definitions')
- .action(lazy(() => import('./migrate/packageExports'), 'command'));
-
- command
- .command('package-lint-configs')
- .description(
- 'Migrates all packages to use @backstage/cli/config/eslint-factory',
- )
- .action(lazy(() => import('./migrate/packageLintConfigs'), 'command'));
-
- command
- .command('react-router-deps')
- .description(
- 'Migrates the react-router dependencies for all packages to be peer dependencies',
- )
- .action(lazy(() => import('./migrate/reactRouterDeps'), 'command'));
+ registerPackageBuildCommands(command);
+ registerPackageTestCommands(command);
+ registerMaintenancePackageCommands(command);
+ registerPackageLintCommands(command);
}
export function registerCommands(program: Command) {
@@ -281,65 +121,13 @@ export function registerCommands(program: Command) {
registerRepoCommand(program);
registerScriptCommand(program);
registerMigrateCommand(program);
-
- program
- .command('versions:bump')
- .option(
- '--pattern ',
- 'Override glob for matching packages to upgrade',
- )
- .option(
- '--release ',
- 'Bump to a specific Backstage release line or version',
- 'main',
- )
- .option('--skip-install', 'Skips yarn install step')
- .option('--skip-migrate', 'Skips migration of any moved packages')
- .description('Bump Backstage packages to the latest versions')
- .action(lazy(() => import('./versions/bump'), 'default'));
-
- program
- .command('versions:migrate')
- .option(
- '--pattern ',
- 'Override glob for matching packages to upgrade',
- )
- .option(
- '--skip-code-changes',
- 'Skip code changes and only update package.json files',
- )
- .description(
- 'Migrate any plugins that have been moved to the @backstage-community namespace automatically',
- )
- .action(lazy(() => import('./versions/migrate'), 'default'));
-
- program
- .command('build-workspace [packages...]')
- .addOption(
- new Option(
- '--alwaysYarnPack',
- 'Alias for --alwaysPack for backwards compatibility.',
- )
- .implies({ alwaysPack: true })
- .hideHelp(true),
- )
- .option(
- '--alwaysPack',
- 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)',
- )
- .description('Builds a temporary dist workspace from the provided packages')
- .action(lazy(() => import('./buildWorkspace'), 'default'));
-
+ registerBuildCommands(program);
+ registerInfoCommands(program);
program
.command('create-github-app ')
.description('Create new GitHub App in your organization.')
.action(lazy(() => import('./create-github-app'), 'default'));
- program
- .command('info')
- .description('Show helpful information for debugging and reporting bugs')
- .action(lazy(() => import('./info'), 'default'));
-
// Notifications for removed commands
program
.command('create')
diff --git a/packages/cli/src/commands/new/new.test.ts b/packages/cli/src/commands/new/new.test.ts
new file mode 100644
index 0000000000..e1cb3b0ffa
--- /dev/null
+++ b/packages/cli/src/commands/new/new.test.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 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 { createNewPackage } from '../../lib/new/createNewPackage';
+import { default as newCommand } from './new';
+
+jest.mock('../../lib/new/createNewPackage');
+
+describe.each([
+ [undefined, undefined, undefined],
+ ['internal', '@internal/', 'backstage-plugin-'],
+ ['internal/', '@internal/', 'backstage-plugin-'],
+ ['acme-backstage', '@acme-backstage/', 'plugin-'],
+ ['acme-backstage/', '@acme-backstage/', 'plugin-'],
+ ['acme-backstage-plugins', '@acme-backstage-plugins/', 'plugin-'],
+])('new', (scope, prefix, infix) => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it(`should generate naming options for --scope=${scope}`, async () => {
+ await newCommand({ scope, option: [], skipInstall: false });
+ expect(createNewPackage).toHaveBeenCalledWith(
+ expect.objectContaining({
+ configOverrides: {
+ packageNamePrefix: prefix,
+ packageNamePluginInfix: infix,
+ },
+ }),
+ );
+ });
+});
diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts
index 5c06555905..970034429c 100644
--- a/packages/cli/src/commands/new/new.ts
+++ b/packages/cli/src/commands/new/new.ts
@@ -42,10 +42,7 @@ export default async (opts: ArgOptions) => {
let pluginInfix: string | undefined = undefined;
let packagePrefix: string | undefined = undefined;
if (scope) {
- const backstagePrefix = scope.startsWith('backstage') ? '' : 'backstage-';
- packagePrefix = scope.includes('/')
- ? `@${scope}${backstagePrefix}`
- : `@${scope}/${backstagePrefix}`;
+ packagePrefix = scope.includes('/') ? `@${scope}` : `@${scope}/`;
pluginInfix = scope.includes('backstage') ? 'plugin-' : 'backstage-plugin-';
}
diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts
index d47f539527..e5b3b0dfbc 100644
--- a/packages/cli/src/commands/start/startFrontend.ts
+++ b/packages/cli/src/commands/start/startFrontend.ts
@@ -16,7 +16,10 @@
import { readJson } from 'fs-extra';
import { resolve as resolvePath } from 'path';
-import { getModuleFederationOptions, serveBundle } from '../../lib/bundler';
+import {
+ getModuleFederationOptions,
+ serveBundle,
+} from '../../modules/build/lib/bundler';
import { paths } from '../../lib/paths';
import { BackstagePackageJson } from '@backstage/cli-node';
diff --git a/packages/cli/src/lib/runner/runBackend.ts b/packages/cli/src/lib/runner/runBackend.ts
index fb8fdd9d55..c085e183f1 100644
--- a/packages/cli/src/lib/runner/runBackend.ts
+++ b/packages/cli/src/lib/runner/runBackend.ts
@@ -39,7 +39,7 @@ export type RunBackendOptions = {
/** Whether to forward the --inspect-brk flag to the node process */
inspectBrkEnabled: boolean;
/** Additional module to require via the --require flag to the node process */
- require?: string;
+ require?: string | string[];
/** An external linked workspace to override module resolution towards */
linkedWorkspace?: string;
};
@@ -107,7 +107,10 @@ export async function runBackend(options: RunBackendOptions) {
optionArgs.push(inspect);
}
if (options.require) {
- optionArgs.push(`--require=${options.require}`);
+ const requires = [options.require].flat();
+ for (const r of requires) {
+ optionArgs.push(`--require=${r}`);
+ }
}
const userArgs = process.argv
diff --git a/packages/cli/src/modules/build/alpha.ts b/packages/cli/src/modules/build/alpha.ts
new file mode 100644
index 0000000000..5e2c6d6e7d
--- /dev/null
+++ b/packages/cli/src/modules/build/alpha.ts
@@ -0,0 +1,115 @@
+/*
+ * 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 { Command, Option } from 'commander';
+import { createCliPlugin } from '../../wiring/factory';
+import { lazy } from '../../lib/lazy';
+import { registerPackageCommands } from '.';
+
+export const buildPlugin = createCliPlugin({
+ pluginId: 'build',
+ init: async reg => {
+ reg.addCommand({
+ path: ['package', 'build'],
+ description: 'Build a package for production deployment or publishing',
+ execute: async ({ args }) => {
+ const command = new Command();
+
+ const defaultCommand = command
+ .option(
+ '--role ',
+ 'Run the command with an explicit package role',
+ )
+ .option(
+ '--minify',
+ 'Minify the generated code. Does not apply to app package (app is minified by default).',
+ )
+ .option(
+ '--skip-build-dependencies',
+ 'Skip the automatic building of local dependencies. Applies to backend packages only.',
+ )
+ .option(
+ '--stats',
+ 'If bundle stats are available, write them to the output directory. Applies to app packages only.',
+ )
+ .option(
+ '--config ',
+ 'Config files to load instead of app-config.yaml. Applies to app packages only.',
+ (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
+ Array(),
+ )
+ .action(lazy(() => import('./commands/package/build'), 'command'));
+ await defaultCommand.parseAsync(args, { from: 'user' });
+ },
+ });
+
+ reg.addCommand({
+ path: ['repo', 'build'],
+ description:
+ 'Build packages in the project, excluding bundled app and backend packages.',
+ execute: async ({ args }) => {
+ const command = new Command();
+
+ // This command expect `package build` to be registered, as its used to parse
+ // individual plugins' package build scripts.
+ registerPackageCommands(command.command('package'));
+
+ const defaultCommand = command
+ .option(
+ '--all',
+ 'Build all packages, including bundled app and backend packages.',
+ )
+ .option(
+ '--since [',
+ 'Only build packages and their dev dependents that changed since the specified ref',
+ )
+ .option(
+ '--minify',
+ 'Minify the generated code. Does not apply to app package (app is minified by default).',
+ )
+ .action(lazy(() => import('./commands/repo/build'), 'command'));
+ await defaultCommand.parseAsync(args, { from: 'user' });
+ },
+ });
+
+ reg.addCommand({
+ path: ['build-workspace'],
+ description:
+ 'Builds a temporary dist workspace from the provided packages',
+ execute: async ({ args }) => {
+ const command = new Command();
+ const defaultCommand = command
+ .arguments('] [packages...]')
+ .addOption(
+ new Option(
+ '--alwaysYarnPack',
+ 'Alias for --alwaysPack for backwards compatibility.',
+ )
+ .implies({ alwaysPack: true })
+ .hideHelp(true),
+ )
+ .option(
+ '--alwaysPack',
+ 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)',
+ )
+ .action(lazy(() => import('./commands/buildWorkspace'), 'default'));
+ await defaultCommand.parseAsync(args, { from: 'user' });
+ },
+ });
+ },
+});
+
+export default buildPlugin;
diff --git a/packages/cli/src/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts
similarity index 100%
rename from packages/cli/src/commands/buildWorkspace.ts
rename to packages/cli/src/modules/build/commands/buildWorkspace.ts
diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/modules/build/commands/package/build/command.ts
similarity index 87%
rename from packages/cli/src/commands/build/command.ts
rename to packages/cli/src/modules/build/commands/package/build/command.ts
index 2784d42d96..77e48841cf 100644
--- a/packages/cli/src/commands/build/command.ts
+++ b/packages/cli/src/modules/build/commands/package/build/command.ts
@@ -15,13 +15,13 @@
*/
import { OptionValues } from 'commander';
-import { buildPackage, Output } from '../../lib/builder';
-import { findRoleFromCommand } from '../../lib/role';
+import { buildPackage, Output } from '../../../lib/builder';
+import { findRoleFromCommand } from '../../../../../lib/role';
import { PackageGraph, PackageRoles } from '@backstage/cli-node';
-import { paths } from '../../lib/paths';
-import { buildFrontend } from './buildFrontend';
-import { buildBackend } from './buildBackend';
-import { isValidUrl } from '../../lib/urls';
+import { paths } from '../../../../../lib/paths';
+import { buildFrontend } from '../../../lib/buildFrontend';
+import { buildBackend } from '../../../lib/buildBackend';
+import { isValidUrl } from '../../../../../lib/urls';
import chalk from 'chalk';
export async function command(opts: OptionValues): Promise {
diff --git a/packages/cli/src/commands/build/index.ts b/packages/cli/src/modules/build/commands/package/build/index.ts
similarity index 100%
rename from packages/cli/src/commands/build/index.ts
rename to packages/cli/src/modules/build/commands/package/build/index.ts
diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/modules/build/commands/repo/build.ts
similarity index 93%
rename from packages/cli/src/commands/repo/build.ts
rename to packages/cli/src/modules/build/commands/repo/build.ts
index cd5e32dd06..44fbc13887 100644
--- a/packages/cli/src/commands/repo/build.ts
+++ b/packages/cli/src/modules/build/commands/repo/build.ts
@@ -18,16 +18,16 @@ import chalk from 'chalk';
import { Command, OptionValues } from 'commander';
import { relative as relativePath } from 'path';
import { buildPackages, getOutputsForRole } from '../../lib/builder';
-import { paths } from '../../lib/paths';
+import { paths } from '../../../../lib/paths';
import {
BackstagePackage,
PackageGraph,
PackageRoles,
} from '@backstage/cli-node';
-import { runParallelWorkers } from '../../lib/parallel';
-import { buildFrontend } from '../build/buildFrontend';
-import { buildBackend } from '../build/buildBackend';
-import { createScriptOptionsParser } from './optionsParser';
+import { runParallelWorkers } from '../../../../lib/parallel';
+import { buildFrontend } from '../../lib/buildFrontend';
+import { buildBackend } from '../../lib/buildBackend';
+import { createScriptOptionsParser } from '../../../../commands/repo/optionsParser';
export async function command(opts: OptionValues, cmd: Command): Promise {
let packages = await PackageGraph.listTargetPackages();
diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts
new file mode 100644
index 0000000000..0135517cd8
--- /dev/null
+++ b/packages/cli/src/modules/build/index.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { Command, Option } from 'commander';
+import { lazy } from '../../lib/lazy';
+
+export function registerRepoCommands(command: Command) {
+ command
+ .command('build')
+ .description(
+ 'Build packages in the project, excluding bundled app and backend packages.',
+ )
+ .option(
+ '--all',
+ 'Build all packages, including bundled app and backend packages.',
+ )
+ .option(
+ '--since [',
+ 'Only build packages and their dev dependents that changed since the specified ref',
+ )
+ .option(
+ '--minify',
+ 'Minify the generated code. Does not apply to app package (app is minified by default).',
+ )
+ .action(lazy(() => import('./commands/repo/build'), 'command'));
+}
+
+export function registerPackageCommands(command: Command) {
+ command
+ .command('build')
+ .description('Build a package for production deployment or publishing')
+ .option('--role ]', 'Run the command with an explicit package role')
+ .option(
+ '--minify',
+ 'Minify the generated code. Does not apply to app package (app is minified by default).',
+ )
+ .option(
+ '--skip-build-dependencies',
+ 'Skip the automatic building of local dependencies. Applies to backend packages only.',
+ )
+ .option(
+ '--stats',
+ 'If bundle stats are available, write them to the output directory. Applies to app packages only.',
+ )
+ .option(
+ '--config ',
+ 'Config files to load instead of app-config.yaml. Applies to app packages only.',
+ (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
+ Array(),
+ )
+ .action(lazy(() => import('./commands/package/build'), 'command'));
+}
+
+export function registerCommands(program: Command) {
+ program
+ .command('build-workspace [packages...]')
+ .addOption(
+ new Option(
+ '--alwaysYarnPack',
+ 'Alias for --alwaysPack for backwards compatibility.',
+ )
+ .implies({ alwaysPack: true })
+ .hideHelp(true),
+ )
+ .option(
+ '--alwaysPack',
+ 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)',
+ )
+ .description('Builds a temporary dist workspace from the provided packages')
+ .action(lazy(() => import('./commands/buildWorkspace'), 'default'));
+}
diff --git a/packages/cli/src/commands/build/buildBackend.ts b/packages/cli/src/modules/build/lib/buildBackend.ts
similarity index 93%
rename from packages/cli/src/commands/build/buildBackend.ts
rename to packages/cli/src/modules/build/lib/buildBackend.ts
index 88d322d482..e109402da1 100644
--- a/packages/cli/src/commands/build/buildBackend.ts
+++ b/packages/cli/src/modules/build/lib/buildBackend.ts
@@ -18,9 +18,9 @@ import os from 'os';
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import tar, { CreateOptions } from 'tar';
-import { createDistWorkspace } from '../../lib/packager';
-import { getEnvironmentParallelism } from '../../lib/parallel';
-import { buildPackage, Output } from '../../lib/builder';
+import { createDistWorkspace } from './packager';
+import { getEnvironmentParallelism } from '../../../lib/parallel';
+import { buildPackage, Output } from './builder';
import { PackageGraph } from '@backstage/cli-node';
const BUNDLE_FILE = 'bundle.tar.gz';
diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/modules/build/lib/buildFrontend.ts
similarity index 88%
rename from packages/cli/src/commands/build/buildFrontend.ts
rename to packages/cli/src/modules/build/lib/buildFrontend.ts
index ec89008b81..6ff04cdb53 100644
--- a/packages/cli/src/commands/build/buildFrontend.ts
+++ b/packages/cli/src/modules/build/lib/buildFrontend.ts
@@ -16,9 +16,9 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
-import { buildBundle, getModuleFederationOptions } from '../../lib/bundler';
-import { getEnvironmentParallelism } from '../../lib/parallel';
-import { loadCliConfig } from '../../modules/config/lib/config';
+import { buildBundle, getModuleFederationOptions } from './bundler';
+import { getEnvironmentParallelism } from '../../../lib/parallel';
+import { loadCliConfig } from '../../config/lib/config';
import { BackstagePackageJson } from '@backstage/cli-node';
interface BuildAppOptions {
diff --git a/packages/cli/src/lib/builder/config.test.ts b/packages/cli/src/modules/build/lib/builder/config.test.ts
similarity index 100%
rename from packages/cli/src/lib/builder/config.test.ts
rename to packages/cli/src/modules/build/lib/builder/config.test.ts
diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/modules/build/lib/builder/config.ts
similarity index 98%
rename from packages/cli/src/lib/builder/config.ts
rename to packages/cli/src/modules/build/lib/builder/config.ts
index 3b55a5a451..1bd61b84f0 100644
--- a/packages/cli/src/lib/builder/config.ts
+++ b/packages/cli/src/modules/build/lib/builder/config.ts
@@ -38,10 +38,10 @@ import {
import { forwardFileImports } from './plugins';
import { BuildOptions, Output } from './types';
-import { paths } from '../paths';
+import { paths } from '../../../../lib/paths';
import { BackstagePackageJson } from '@backstage/cli-node';
-import { svgrTemplate } from '../svgrTemplate';
-import { readEntryPoints } from '../entryPoints';
+import { svgrTemplate } from '../../../../lib/svgrTemplate';
+import { readEntryPoints } from '../../../../lib/entryPoints';
const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx'];
diff --git a/packages/cli/src/lib/builder/index.ts b/packages/cli/src/modules/build/lib/builder/index.ts
similarity index 100%
rename from packages/cli/src/lib/builder/index.ts
rename to packages/cli/src/modules/build/lib/builder/index.ts
diff --git a/packages/cli/src/lib/builder/packager.test.ts b/packages/cli/src/modules/build/lib/builder/packager.test.ts
similarity index 100%
rename from packages/cli/src/lib/builder/packager.test.ts
rename to packages/cli/src/modules/build/lib/builder/packager.test.ts
diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/modules/build/lib/builder/packager.ts
similarity index 97%
rename from packages/cli/src/lib/builder/packager.ts
rename to packages/cli/src/modules/build/lib/builder/packager.ts
index b709cb0408..dacf5eef63 100644
--- a/packages/cli/src/lib/builder/packager.ts
+++ b/packages/cli/src/modules/build/lib/builder/packager.ts
@@ -18,11 +18,11 @@ import fs from 'fs-extra';
import { rollup, RollupOptions } from 'rollup';
import chalk from 'chalk';
import { relative as relativePath, resolve as resolvePath } from 'path';
-import { paths } from '../paths';
+import { paths } from '../../../../lib/paths';
import { makeRollupConfigs } from './config';
import { BuildOptions, Output } from './types';
import { PackageRoles } from '@backstage/cli-node';
-import { runParallelWorkers } from '../parallel';
+import { runParallelWorkers } from '../../../../lib/parallel';
export function formatErrorMessage(error: any) {
let msg = '';
diff --git a/packages/cli/src/lib/builder/plugins.test.ts b/packages/cli/src/modules/build/lib/builder/plugins.test.ts
similarity index 100%
rename from packages/cli/src/lib/builder/plugins.test.ts
rename to packages/cli/src/modules/build/lib/builder/plugins.test.ts
diff --git a/packages/cli/src/lib/builder/plugins.ts b/packages/cli/src/modules/build/lib/builder/plugins.ts
similarity index 100%
rename from packages/cli/src/lib/builder/plugins.ts
rename to packages/cli/src/modules/build/lib/builder/plugins.ts
diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/modules/build/lib/builder/types.ts
similarity index 100%
rename from packages/cli/src/lib/builder/types.ts
rename to packages/cli/src/modules/build/lib/builder/types.ts
diff --git a/packages/cli/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts b/packages/cli/src/modules/build/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts
similarity index 100%
rename from packages/cli/src/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts
rename to packages/cli/src/modules/build/lib/bundler/ConfigInjectingHtmlWebpackPlugin.ts
diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/modules/build/lib/bundler/bundle.ts
similarity index 100%
rename from packages/cli/src/lib/bundler/bundle.ts
rename to packages/cli/src/modules/build/lib/bundler/bundle.ts
diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/modules/build/lib/bundler/config.ts
similarity index 98%
rename from packages/cli/src/lib/bundler/config.ts
rename to packages/cli/src/modules/build/lib/bundler/config.ts
index 811d032380..6b7b6543ae 100644
--- a/packages/cli/src/lib/bundler/config.ts
+++ b/packages/cli/src/modules/build/lib/bundler/config.ts
@@ -27,13 +27,13 @@ import HtmlWebpackPlugin from 'html-webpack-plugin';
import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin';
-import { paths as cliPaths } from '../../lib/paths';
+import { paths as cliPaths } from '../../../../lib/paths';
import fs from 'fs-extra';
import { optimization as optimizationConfig } from './optimization';
import pickBy from 'lodash/pickBy';
-import { runPlain } from '../run';
+import { runPlain } from '../../../../lib/run';
import { transforms } from './transforms';
-import { version } from '../../lib/version';
+import { version } from '../../../../lib/version';
import yn from 'yn';
import { hasReactDomClient } from './hasReactDomClient';
import { createWorkspaceLinkingPlugins } from './linkWorkspaces';
diff --git a/packages/cli/src/lib/bundler/hasReactDomClient.ts b/packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts
similarity index 94%
rename from packages/cli/src/lib/bundler/hasReactDomClient.ts
rename to packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts
index 67e6a3f42e..ad331bdf8b 100644
--- a/packages/cli/src/lib/bundler/hasReactDomClient.ts
+++ b/packages/cli/src/modules/build/lib/bundler/hasReactDomClient.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { paths } from '../paths';
+import { paths } from '../../../../lib/paths';
export function hasReactDomClient() {
try {
diff --git a/packages/cli/src/lib/bundler/index.ts b/packages/cli/src/modules/build/lib/bundler/index.ts
similarity index 100%
rename from packages/cli/src/lib/bundler/index.ts
rename to packages/cli/src/modules/build/lib/bundler/index.ts
diff --git a/packages/cli/src/lib/bundler/linkWorkspaces.ts b/packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts
similarity index 97%
rename from packages/cli/src/lib/bundler/linkWorkspaces.ts
rename to packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts
index 2fe0eb2fb3..a2fe6cd25a 100644
--- a/packages/cli/src/lib/bundler/linkWorkspaces.ts
+++ b/packages/cli/src/modules/build/lib/bundler/linkWorkspaces.ts
@@ -17,7 +17,7 @@
import { relative as relativePath } from 'path';
import { getPackages } from '@manypkg/get-packages';
import webpack from 'webpack';
-import { paths } from '../paths';
+import { paths } from '../../../../lib/paths';
/**
* This returns of collection of plugins that links a separate workspace into
diff --git a/packages/cli/src/lib/bundler/moduleFederation.ts b/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts
similarity index 96%
rename from packages/cli/src/lib/bundler/moduleFederation.ts
rename to packages/cli/src/modules/build/lib/bundler/moduleFederation.ts
index 26d1c4784b..0bc606f82a 100644
--- a/packages/cli/src/lib/bundler/moduleFederation.ts
+++ b/packages/cli/src/modules/build/lib/bundler/moduleFederation.ts
@@ -17,11 +17,11 @@
import chalk from 'chalk';
import { ModuleFederationOptions } from './types';
import { BackstagePackageJson } from '@backstage/cli-node';
-import { readEntryPoints } from '../entryPoints';
+import { readEntryPoints } from '../../../../lib/entryPoints';
import {
createTypeDistProject,
getEntryPointDefaultFeatureType,
-} from '../typeDistProject';
+} from '../../../../lib/typeDistProject';
export async function getModuleFederationOptions(
packageJson: BackstagePackageJson,
diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/modules/build/lib/bundler/optimization.ts
similarity index 100%
rename from packages/cli/src/lib/bundler/optimization.ts
rename to packages/cli/src/modules/build/lib/bundler/optimization.ts
diff --git a/packages/cli/src/lib/bundler/packageDetection.ts b/packages/cli/src/modules/build/lib/bundler/packageDetection.ts
similarity index 98%
rename from packages/cli/src/lib/bundler/packageDetection.ts
rename to packages/cli/src/modules/build/lib/bundler/packageDetection.ts
index 0c357fe79e..f69333abb3 100644
--- a/packages/cli/src/lib/bundler/packageDetection.ts
+++ b/packages/cli/src/modules/build/lib/bundler/packageDetection.ts
@@ -20,7 +20,7 @@ import chokidar from 'chokidar';
import fs from 'fs-extra';
import PQueue from 'p-queue';
import { join as joinPath, resolve as resolvePath } from 'path';
-import { paths as cliPaths } from '../paths';
+import { paths as cliPaths } from '../../../../lib/paths';
const DETECTED_MODULES_MODULE_NAME = '__backstage-autodetected-plugins__';
diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/modules/build/lib/bundler/paths.ts
similarity index 98%
rename from packages/cli/src/lib/bundler/paths.ts
rename to packages/cli/src/modules/build/lib/bundler/paths.ts
index 572a96337f..06f369ac6e 100644
--- a/packages/cli/src/lib/bundler/paths.ts
+++ b/packages/cli/src/modules/build/lib/bundler/paths.ts
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
-import { paths } from '../paths';
+import { paths } from '../../../../lib/paths';
export type BundlingPathsOptions = {
// bundle entrypoint, e.g. 'src/index'
diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/modules/build/lib/bundler/server.ts
similarity index 98%
rename from packages/cli/src/lib/bundler/server.ts
rename to packages/cli/src/modules/build/lib/bundler/server.ts
index 2931b3f53e..ea6ee908b4 100644
--- a/packages/cli/src/lib/bundler/server.ts
+++ b/packages/cli/src/modules/build/lib/bundler/server.ts
@@ -21,8 +21,8 @@ import openBrowser from 'react-dev-utils/openBrowser';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
-import { paths as libPaths } from '../../lib/paths';
-import { loadCliConfig } from '../../modules/config/lib/config';
+import { paths as libPaths } from '../../../../lib/paths';
+import { loadCliConfig } from '../../../config/lib/config';
import { createConfig, resolveBaseUrl, resolveEndpoint } from './config';
import { createDetectedModulesEntryPoint } from './packageDetection';
import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths';
diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/modules/build/lib/bundler/transforms.ts
similarity index 98%
rename from packages/cli/src/lib/bundler/transforms.ts
rename to packages/cli/src/modules/build/lib/bundler/transforms.ts
index f0a8a9d01d..e4502bad0d 100644
--- a/packages/cli/src/lib/bundler/transforms.ts
+++ b/packages/cli/src/modules/build/lib/bundler/transforms.ts
@@ -16,7 +16,7 @@
import { RuleSetRule, WebpackPluginInstance } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
-import { svgrTemplate } from '../svgrTemplate';
+import { svgrTemplate } from '../../../../lib/svgrTemplate';
type Transforms = {
loaders: RuleSetRule[];
diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/modules/build/lib/bundler/types.ts
similarity index 100%
rename from packages/cli/src/lib/bundler/types.ts
rename to packages/cli/src/modules/build/lib/bundler/types.ts
diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts
similarity index 97%
rename from packages/cli/src/lib/packager/createDistWorkspace.ts
rename to packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts
index 9ca3f7af70..971409cd93 100644
--- a/packages/cli/src/lib/packager/createDistWorkspace.ts
+++ b/packages/cli/src/modules/build/lib/packager/createDistWorkspace.ts
@@ -24,12 +24,12 @@ import {
import { tmpdir } from 'os';
import tar, { CreateOptions, FileOptions } from 'tar';
import partition from 'lodash/partition';
-import { paths } from '../paths';
-import { run } from '../run';
+import { paths } from '../../../../lib/paths';
+import { run } from '../../../../lib/run';
import {
dependencies as cliDependencies,
devDependencies as cliDevDependencies,
-} from '../../../package.json';
+} from '../../../../../package.json';
import {
BuildOptions,
buildPackages,
@@ -42,8 +42,8 @@ import {
PackageGraph,
PackageGraphNode,
} from '@backstage/cli-node';
-import { runParallelWorkers } from '../parallel';
-import { createTypeDistProject } from '../typeDistProject';
+import { runParallelWorkers } from '../../../../lib/parallel';
+import { createTypeDistProject } from '../../../../lib/typeDistProject';
// These packages aren't safe to pack in parallel since the CLI depends on them
const UNSAFE_PACKAGES = [
diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/modules/build/lib/packager/index.ts
similarity index 100%
rename from packages/cli/src/lib/packager/index.ts
rename to packages/cli/src/modules/build/lib/packager/index.ts
diff --git a/packages/cli/src/lib/packager/productionPack.ts b/packages/cli/src/modules/build/lib/packager/productionPack.ts
similarity index 92%
rename from packages/cli/src/lib/packager/productionPack.ts
rename to packages/cli/src/modules/build/lib/packager/productionPack.ts
index bfbafc9d27..b201d24d0c 100644
--- a/packages/cli/src/lib/packager/productionPack.ts
+++ b/packages/cli/src/modules/build/lib/packager/productionPack.ts
@@ -18,8 +18,8 @@ import fs from 'fs-extra';
import npmPackList from 'npm-packlist';
import { resolve as resolvePath, posix as posixPath } from 'path';
import { BackstagePackageJson } from '@backstage/cli-node';
-import { readEntryPoints } from '../entryPoints';
-import { getEntryPointDefaultFeatureType } from '../typeDistProject';
+import { readEntryPoints } from '../../../../lib/entryPoints';
+import { getEntryPointDefaultFeatureType } from '../../../../lib/typeDistProject';
import { Project } from 'ts-morph';
const PKG_PATH = 'package.json';
@@ -143,7 +143,7 @@ async function rewriteEntryPoints(
// Clear to ensure a clean slate before adding entries back in further down
if (pkg.typesVersions) {
- pkg.typesVersions = { '*': {} };
+ pkg.typesVersions = undefined;
}
for (const entryPoint of entryPoints) {
@@ -166,9 +166,8 @@ async function rewriteEntryPoints(
if (!pkg.typesVersions) {
pkg.typesVersions = { '*': {} };
}
- pkg.typesVersions['*'][entryPoint.name] = [
- `dist/${entryPoint.name}.d.ts`,
- ];
+ const mount = entryPoint.name === 'index' ? '*' : entryPoint.name;
+ pkg.typesVersions['*'][mount] = [`dist/${entryPoint.name}.d.ts`];
}
exp.default = exp.require ?? exp.import;
@@ -218,6 +217,14 @@ async function rewriteEntryPoints(
}
}
+ // Clean up the typesVersions field if it only contains a wildcard
+ if (pkg.typesVersions?.['*']) {
+ const keys = Object.keys(pkg.typesVersions['*']);
+ if (keys.length === 1 && keys[0] === '*') {
+ delete pkg.typesVersions;
+ }
+ }
+
if (pkg.exports) {
pkg.exports = outputExports;
// We treat package.json as a fixed export that is always available in the published package
diff --git a/packages/cli/src/modules/config/alpha.ts b/packages/cli/src/modules/config/alpha.ts
index 9ce6080a4a..8e5ff8b783 100644
--- a/packages/cli/src/modules/config/alpha.ts
+++ b/packages/cli/src/modules/config/alpha.ts
@@ -47,7 +47,8 @@ export default createCliPlugin({
})
.help()
.parse(args);
- const m = await import('./commands/docs');
+ const m =
+ (await require('./commands/docs')) as typeof import('./commands/docs');
await m.default(argv);
},
});
@@ -66,7 +67,8 @@ export default createCliPlugin({
})
.help()
.parse(args);
- const m = await import('./commands/print');
+ const m =
+ (await require('./commands/print')) as typeof import('./commands/print');
await m.default(argv);
},
});
@@ -90,7 +92,8 @@ export default createCliPlugin({
})
.help()
.parse(args);
- const m = await import('./commands/validate');
+ const m =
+ (await require('./commands/validate')) as typeof import('./commands/validate');
await m.default(argv);
},
});
diff --git a/canon-docs/src/utils/spaceProps.ts b/packages/cli/src/modules/info/alpha.ts
similarity index 54%
rename from canon-docs/src/utils/spaceProps.ts
rename to packages/cli/src/modules/info/alpha.ts
index 3eb6961bc1..1e6f761f83 100644
--- a/canon-docs/src/utils/spaceProps.ts
+++ b/packages/cli/src/modules/info/alpha.ts
@@ -13,29 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import yargs from 'yargs';
+import { createCliPlugin } from '../../wiring/factory';
-export const spacePropsList = [
- 'margin',
- 'marginBottom',
- 'marginLeft',
- 'marginRight',
- 'marginTop',
- 'marginX',
- 'marginY',
- 'padding',
- 'paddingBottom',
- 'paddingLeft',
- 'paddingRight',
- 'paddingTop',
- 'paddingX',
- 'paddingY',
-].reduce(
- (acc: { [key: string]: { type: string[]; responsive: boolean } }, prop) => {
- acc[prop] = {
- type: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl'],
- responsive: true,
- };
- return acc;
+export default createCliPlugin({
+ pluginId: 'info',
+ init: async reg => {
+ reg.addCommand({
+ path: ['info'],
+ description: 'Show helpful information for debugging and reporting bugs',
+ execute: async ({ args }) => {
+ yargs().parse(args);
+ const { default: command } =
+ require('./commands/info') as typeof import('./commands/info');
+ await command();
+ },
+ });
},
- {},
-);
+});
diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts
similarity index 89%
rename from packages/cli/src/commands/info.ts
rename to packages/cli/src/modules/info/commands/info.ts
index f3740de1a9..416c995541 100644
--- a/packages/cli/src/commands/info.ts
+++ b/packages/cli/src/modules/info/commands/info.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Backstage Authors
+ * Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-import { version as cliVersion } from '../../package.json';
+import { version as cliVersion } from '../../../../package.json';
import os from 'os';
-import { runPlain } from '../lib/run';
-import { paths } from '../lib/paths';
-import { Lockfile } from '../lib/versioning';
+import { runPlain } from '../../../lib/run';
+import { paths } from '../../../lib/paths';
+import { Lockfile } from '../../../lib/versioning';
import fs from 'fs-extra';
export default async () => {
diff --git a/packages/backend-legacy/src/plugins/proxy.ts b/packages/cli/src/modules/info/index.ts
similarity index 63%
rename from packages/backend-legacy/src/plugins/proxy.ts
rename to packages/cli/src/modules/info/index.ts
index 273e791f1c..af43af652c 100644
--- a/packages/backend-legacy/src/plugins/proxy.ts
+++ b/packages/cli/src/modules/info/index.ts
@@ -14,16 +14,12 @@
* limitations under the License.
*/
-import { createRouter } from '@backstage/plugin-proxy-backend';
-import { Router } from 'express';
-import { PluginEnvironment } from '../types';
+import { lazy } from '../../lib/lazy';
+import { Command } from 'commander';
-export default async function createPlugin(
- env: PluginEnvironment,
-): Promise {
- return await createRouter({
- logger: env.logger,
- config: env.config,
- discovery: env.discovery,
- });
+export function registerCommands(program: Command) {
+ program
+ .command('info')
+ .description('Show helpful information for debugging and reporting bugs')
+ .action(lazy(() => import('./commands/info'), 'default'));
}
diff --git a/packages/cli/src/modules/lint/alpha.ts b/packages/cli/src/modules/lint/alpha.ts
new file mode 100644
index 0000000000..617f71d5c9
--- /dev/null
+++ b/packages/cli/src/modules/lint/alpha.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2025 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 { createCliPlugin } from '../../wiring/factory';
+import { Command } from 'commander';
+import { lazy } from '../../lib/lazy';
+
+export default createCliPlugin({
+ pluginId: 'lint',
+ init: async reg => {
+ reg.addCommand({
+ path: ['package', 'lint'],
+ description: 'Lint a package',
+ execute: async ({ args }) => {
+ const command = new Command();
+ command.arguments('[directories...]');
+ command.option('--fix', 'Attempt to automatically fix violations');
+ command.option(
+ '--format ',
+ 'Lint report output format',
+ 'eslint-formatter-friendly',
+ );
+ command.option(
+ '--output-file ',
+ 'Write the lint report to a file instead of stdout',
+ );
+ command.option(
+ '--max-warnings ',
+ 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
+ );
+ command.description('Lint a package');
+ command.action(
+ lazy(() => import('./commands/package/lint'), 'default'),
+ );
+
+ await command.parseAsync(args, { from: 'user' });
+ },
+ });
+
+ reg.addCommand({
+ path: ['repo', 'lint'],
+ description: 'Lint a repository',
+ execute: async ({ args }) => {
+ const command = new Command();
+ command.option('--fix', 'Attempt to automatically fix violations');
+ command.option(
+ '--format ',
+ 'Lint report output format',
+ 'eslint-formatter-friendly',
+ );
+ command.option(
+ '--output-file ',
+ 'Write the lint report to a file instead of stdout',
+ );
+ command.option(
+ '--successCache',
+ 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
+ );
+ command.option(
+ '--successCacheDir ',
+ 'Set the success cache location, (default: node_modules/.cache/backstage-cli)',
+ );
+ command.option(
+ '--since [',
+ 'Only lint packages that changed since the specified ref',
+ );
+ command.option(
+ '--max-warnings ]',
+ 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
+ );
+ command.description('Lint a repository');
+ command.action(lazy(() => import('./commands/repo/lint'), 'command'));
+
+ await command.parseAsync(args, { from: 'user' });
+ },
+ });
+ },
+});
diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/modules/lint/commands/package/lint.ts
similarity index 95%
rename from packages/cli/src/commands/lint.ts
rename to packages/cli/src/modules/lint/commands/package/lint.ts
index 8c8877c19c..a1e45c0113 100644
--- a/packages/cli/src/commands/lint.ts
+++ b/packages/cli/src/modules/lint/commands/package/lint.ts
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { OptionValues } from 'commander';
-import { paths } from '../lib/paths';
+import { paths } from '../../../../lib/paths';
import { ESLint } from 'eslint';
export default async (directories: string[], opts: OptionValues) => {
@@ -30,7 +30,7 @@ export default async (directories: string[], opts: OptionValues) => {
directories.length ? directories : ['.'],
);
- const maxWarnings = opts.maxWarnings ?? 0;
+ const maxWarnings = opts.maxWarnings ?? -1;
const ignoreWarnings = +maxWarnings === -1;
const failed =
diff --git a/packages/cli/src/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts
similarity index 95%
rename from packages/cli/src/commands/repo/lint.ts
rename to packages/cli/src/modules/lint/commands/repo/lint.ts
index f78b51ea5d..ab6597c70a 100644
--- a/packages/cli/src/commands/repo/lint.ts
+++ b/packages/cli/src/modules/lint/commands/repo/lint.ts
@@ -24,10 +24,10 @@ import {
BackstagePackageJson,
Lockfile,
} from '@backstage/cli-node';
-import { paths } from '../../lib/paths';
-import { runWorkerQueueThreads } from '../../lib/parallel';
-import { createScriptOptionsParser } from './optionsParser';
-import { SuccessCache } from '../../lib/cache/SuccessCache';
+import { paths } from '../../../../lib/paths';
+import { runWorkerQueueThreads } from '../../../../lib/parallel';
+import { createScriptOptionsParser } from '../../../../commands/repo/optionsParser';
+import { SuccessCache } from '../../../../lib/cache/SuccessCache';
function depCount(pkg: BackstagePackageJson) {
const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0;
@@ -111,6 +111,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise {
fix: Boolean(opts.fix),
format: opts.format as string | undefined,
shouldCache: Boolean(cacheContext),
+ maxWarnings: opts.maxWarnings ?? -1,
successCache: cacheContext?.entries,
rootDir: paths.targetRoot,
},
@@ -120,6 +121,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise {
shouldCache,
successCache,
rootDir,
+ maxWarnings,
}) => {
const { ESLint } = require('eslint') as typeof import('eslint');
const crypto = require('crypto') as typeof import('crypto');
@@ -131,7 +133,6 @@ export async function command(opts: OptionValues, cmd: Command): Promise {
return async ({
fullDir,
relativeDir,
- lintOptions,
parentHash,
}): Promise<{
relativeDir: string;
@@ -199,7 +200,6 @@ export async function command(opts: OptionValues, cmd: Command): Promise {
await ESLint.outputFixes(results);
}
- const maxWarnings = lintOptions?.maxWarnings ?? 0;
const ignoreWarnings = +maxWarnings === -1;
const resultText = formatter.format(results) as string;
diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts
new file mode 100644
index 0000000000..dfa0916efb
--- /dev/null
+++ b/packages/cli/src/modules/lint/index.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Command } from 'commander';
+import { lazy } from '../../lib/lazy';
+
+export function registerPackageCommands(command: Command) {
+ command
+ .command('lint [directories...]')
+ .option(
+ '--format ',
+ 'Lint report output format',
+ 'eslint-formatter-friendly',
+ )
+ .option(
+ '--output-file ',
+ 'Write the lint report to a file instead of stdout',
+ )
+ .option('--fix', 'Attempt to automatically fix violations')
+ .option(
+ '--max-warnings ',
+ 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)',
+ )
+ .description('Lint a package')
+ .action(lazy(() => import('./commands/package/lint'), 'default'));
+}
+
+export function registerRepoCommands(command: Command) {
+ command
+ .command('lint')
+ .description('Lint all packages in the project')
+ .option(
+ '--format ',
+ 'Lint report output format',
+ 'eslint-formatter-friendly',
+ )
+ .option(
+ '--output-file ',
+ 'Write the lint report to a file instead of stdout',
+ )
+ .option(
+ '--since [',
+ 'Only lint packages that changed since the specified ref',
+ )
+ .option(
+ '--successCache',
+ 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run',
+ )
+ .option(
+ '--successCacheDir ]