From eeaf3d8b0e56cebada9eebaaeea5f5b55f675f73 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Wed, 17 Jun 2026 11:46:20 -0500 Subject: [PATCH] feat: add v1.52.0 codemods and migration recipe (#97-#101) Five packages for the Backstage 1.52.0 migration: - migrate-bui-props-to-intersection (#97): Convert interface extends ComboboxProps/SelectProps to type intersection. CSS pass adds TODO for .bui-SelectPopover > selectors. 6 tests. - remove-stitching-strategy-mode (#98): Remove deprecated catalog.stitchingStrategy.mode from app-config YAML. 5 tests. - rename-bui-css-tokens-v1-52 (#99): Rename 20 deprecated BUI semantic color tokens + 12 neutral interaction TODO markers. Two-pass workflow (TS + CSS). 7 tests. - migrate-select-combobox-deprecated-props (#100): Migrate deprecated Select searchable/searchPlaceholder and Combobox inputValue/onInputChange to nested search config. Rename inline option value to id. 7 tests. - v1-52-0-migration-recipe (#101): Chains all 4 codemods in order (breaking first, deprecations last). No aiFixup on any step. All transforms use AST-based detection via ast-grep JSSG. Regex is only used for text replacement within already-matched AST nodes. Includes changesets for all 5 packages (minor bump). Closes #97, closes #98, closes #99, closes #100, closes #101 --- .../migrate-bui-props-to-intersection.md | 5 + ...igrate-select-combobox-deprecated-props.md | 5 + .changeset/remove-stitching-strategy-mode.md | 5 + .changeset/rename-bui-css-tokens-v1-52.md | 5 + .changeset/v1-52-0-migration-recipe.md | 5 + README.md | 31 +- .../codemod.yaml | 20 + .../package.json | 13 + .../scripts/codemod-css.ts | 48 +++ .../scripts/codemod.ts | 254 ++++++++++++ .../noop-no-child-combinator/expected.css | 3 + .../noop-no-child-combinator/input.css | 3 + .../select-popover-child/expected.css | 8 + .../tests-css/select-popover-child/input.css | 7 + .../select-popover-child/metrics.json | 10 + .../tests/empty-body/expected.tsx | 3 + .../tests/empty-body/input.tsx | 3 + .../tests/empty-body/metrics.json | 11 + .../tests/multi-extends/expected.tsx | 9 + .../tests/multi-extends/input.tsx | 9 + .../tests/multi-extends/metrics.json | 11 + .../tests/named-import/expected.tsx | 5 + .../tests/named-import/input.tsx | 5 + .../tests/named-import/metrics.json | 11 + .../tests/namespace-import/expected.tsx | 5 + .../tests/namespace-import/input.tsx | 5 + .../tests/namespace-import/metrics.json | 11 + .../tests/noop-non-bui/expected.tsx | 5 + .../tests/noop-non-bui/input.tsx | 5 + .../tsconfig.json | 16 + .../workflow.yaml | 29 ++ .../codemod.yaml | 20 + .../package.json | 13 + .../scripts/codemod.ts | 383 ++++++++++++++++++ .../tests/combobox-input-value/expected.tsx | 9 + .../tests/combobox-input-value/input.tsx | 9 + .../tests/combobox-input-value/metrics.json | 11 + .../tests/existing-search-prop/expected.tsx | 7 + .../tests/existing-search-prop/input.tsx | 7 + .../tests/existing-search-prop/metrics.json | 11 + .../inline-options-value-to-id/expected.tsx | 12 + .../inline-options-value-to-id/input.tsx | 12 + .../inline-options-value-to-id/metrics.json | 11 + .../tests/noop-non-bui/expected.tsx | 7 + .../tests/noop-non-bui/input.tsx | 7 + .../noop-table-searchplaceholder/expected.tsx | 13 + .../noop-table-searchplaceholder/input.tsx | 13 + .../tests/select-searchable/expected.tsx | 7 + .../tests/select-searchable/input.tsx | 7 + .../tests/select-searchable/metrics.json | 11 + .../tests/variable-options-todo/expected.tsx | 7 + .../tests/variable-options-todo/input.tsx | 7 + .../tests/variable-options-todo/metrics.json | 11 + .../tsconfig.json | 16 + .../workflow.yaml | 19 + codemods/v1.52.0/migration-recipe/README.md | 46 +++ .../v1.52.0/migration-recipe/codemod.yaml | 19 + .../v1.52.0/migration-recipe/package.json | 12 + .../v1.52.0/migration-recipe/workflow.yaml | 27 ++ .../codemod.yaml | 20 + .../package.json | 13 + .../scripts/codemod.ts | 220 ++++++++++ .../catalog-only-stitching/expected.yaml | 1 + .../tests/catalog-only-stitching/input.yaml | 3 + .../tests/catalog-only-stitching/metrics.json | 10 + .../tests/deferred-mode-only/expected.yaml | 3 + .../tests/deferred-mode-only/input.yaml | 5 + .../tests/deferred-mode-only/metrics.json | 10 + .../tests/mode-only/expected.yaml | 3 + .../tests/mode-only/input.yaml | 5 + .../tests/mode-only/metrics.json | 10 + .../tests/mode-with-siblings/expected.yaml | 6 + .../tests/mode-with-siblings/input.yaml | 7 + .../tests/mode-with-siblings/metrics.json | 10 + .../tests/no-mode-noop/expected.yaml | 5 + .../tests/no-mode-noop/input.yaml | 5 + .../tests/quoted-mode/expected.yaml | 3 + .../tests/quoted-mode/input.yaml | 5 + .../tests/quoted-mode/metrics.json | 10 + .../tsconfig.json | 16 + .../workflow.yaml | 18 + .../rename-bui-css-tokens-v1-52/codemod.yaml | 20 + .../rename-bui-css-tokens-v1-52/package.json | 13 + .../scripts/codemod-css.ts | 188 +++++++++ .../scripts/codemod.ts | 199 +++++++++ .../tests-css/noop-no-tokens/expected.css | 5 + .../tests-css/noop-no-tokens/input.css | 5 + .../tests-css/token-renames/expected.css | 24 ++ .../tests-css/token-renames/input.css | 24 ++ .../tests-css/token-renames/metrics.json | 64 +++ .../tests/accent-renames/expected.tsx | 7 + .../tests/accent-renames/input.tsx | 7 + .../tests/accent-renames/metrics.json | 34 ++ .../tests/foreground-renames/expected.tsx | 5 + .../tests/foreground-renames/input.tsx | 5 + .../tests/foreground-renames/metrics.json | 22 + .../neutral-interaction-todo/expected.tsx | 5 + .../tests/neutral-interaction-todo/input.tsx | 5 + .../neutral-interaction-todo/metrics.json | 22 + .../tests/noop-no-tokens/expected.tsx | 5 + .../tests/noop-no-tokens/input.tsx | 5 + .../tests/semantic-renames/expected.tsx | 14 + .../tests/semantic-renames/input.tsx | 14 + .../tests/semantic-renames/metrics.json | 76 ++++ .../rename-bui-css-tokens-v1-52/tsconfig.json | 16 + .../rename-bui-css-tokens-v1-52/workflow.yaml | 29 ++ yarn.lock | 44 ++ 107 files changed, 2525 insertions(+), 19 deletions(-) create mode 100644 .changeset/migrate-bui-props-to-intersection.md create mode 100644 .changeset/migrate-select-combobox-deprecated-props.md create mode 100644 .changeset/remove-stitching-strategy-mode.md create mode 100644 .changeset/rename-bui-css-tokens-v1-52.md create mode 100644 .changeset/v1-52-0-migration-recipe.md create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/codemod.yaml create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/package.json create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod-css.ts create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod.ts create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/expected.css create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/input.css create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/expected.css create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/input.css create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/metrics.json create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/expected.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/input.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/metrics.json create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/expected.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/input.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/metrics.json create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/expected.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/input.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/metrics.json create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/expected.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/input.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/metrics.json create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/expected.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/input.tsx create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/tsconfig.json create mode 100644 codemods/v1.52.0/migrate-bui-props-to-intersection/workflow.yaml create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/codemod.yaml create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/package.json create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/scripts/codemod.ts create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/combobox-input-value/expected.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/combobox-input-value/input.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/combobox-input-value/metrics.json create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/existing-search-prop/expected.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/existing-search-prop/input.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/existing-search-prop/metrics.json create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/inline-options-value-to-id/expected.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/inline-options-value-to-id/input.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/inline-options-value-to-id/metrics.json create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/noop-non-bui/expected.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/noop-non-bui/input.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/noop-table-searchplaceholder/expected.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/noop-table-searchplaceholder/input.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/select-searchable/expected.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/select-searchable/input.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/select-searchable/metrics.json create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/variable-options-todo/expected.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/variable-options-todo/input.tsx create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/variable-options-todo/metrics.json create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/tsconfig.json create mode 100644 codemods/v1.52.0/migrate-select-combobox-deprecated-props/workflow.yaml create mode 100644 codemods/v1.52.0/migration-recipe/README.md create mode 100644 codemods/v1.52.0/migration-recipe/codemod.yaml create mode 100644 codemods/v1.52.0/migration-recipe/package.json create mode 100644 codemods/v1.52.0/migration-recipe/workflow.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/codemod.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/package.json create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/scripts/codemod.ts create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/catalog-only-stitching/expected.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/catalog-only-stitching/input.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/catalog-only-stitching/metrics.json create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/deferred-mode-only/expected.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/deferred-mode-only/input.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/deferred-mode-only/metrics.json create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/mode-only/expected.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/mode-only/input.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/mode-only/metrics.json create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/mode-with-siblings/expected.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/mode-with-siblings/input.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/mode-with-siblings/metrics.json create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/no-mode-noop/expected.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/no-mode-noop/input.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/quoted-mode/expected.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/quoted-mode/input.yaml create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tests/quoted-mode/metrics.json create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/tsconfig.json create mode 100644 codemods/v1.52.0/remove-stitching-strategy-mode/workflow.yaml create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/codemod.yaml create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/package.json create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/scripts/codemod-css.ts create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/scripts/codemod.ts create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests-css/noop-no-tokens/expected.css create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests-css/noop-no-tokens/input.css create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests-css/token-renames/expected.css create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests-css/token-renames/input.css create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests-css/token-renames/metrics.json create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/accent-renames/expected.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/accent-renames/input.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/accent-renames/metrics.json create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/foreground-renames/expected.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/foreground-renames/input.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/foreground-renames/metrics.json create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/neutral-interaction-todo/expected.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/neutral-interaction-todo/input.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/neutral-interaction-todo/metrics.json create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/noop-no-tokens/expected.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/noop-no-tokens/input.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/semantic-renames/expected.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/semantic-renames/input.tsx create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tests/semantic-renames/metrics.json create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/tsconfig.json create mode 100644 codemods/v1.52.0/rename-bui-css-tokens-v1-52/workflow.yaml diff --git a/.changeset/migrate-bui-props-to-intersection.md b/.changeset/migrate-bui-props-to-intersection.md new file mode 100644 index 0000000..1a86313 --- /dev/null +++ b/.changeset/migrate-bui-props-to-intersection.md @@ -0,0 +1,5 @@ +--- +'@backstage/migrate-bui-props-to-intersection': minor +--- + +Add codemod to migrate ComboboxProps/SelectProps interface extends to type intersection for Backstage 1.52.0 diff --git a/.changeset/migrate-select-combobox-deprecated-props.md b/.changeset/migrate-select-combobox-deprecated-props.md new file mode 100644 index 0000000..ca3e528 --- /dev/null +++ b/.changeset/migrate-select-combobox-deprecated-props.md @@ -0,0 +1,5 @@ +--- +'@backstage/migrate-select-combobox-deprecated-props': minor +--- + +Add codemod to migrate deprecated Select/Combobox search props and option value to id for Backstage 1.52.0 diff --git a/.changeset/remove-stitching-strategy-mode.md b/.changeset/remove-stitching-strategy-mode.md new file mode 100644 index 0000000..fba0eed --- /dev/null +++ b/.changeset/remove-stitching-strategy-mode.md @@ -0,0 +1,5 @@ +--- +'@backstage/remove-stitching-strategy-mode': minor +--- + +Add codemod to remove deprecated catalog.stitchingStrategy.mode from app-config for Backstage 1.52.0 diff --git a/.changeset/rename-bui-css-tokens-v1-52.md b/.changeset/rename-bui-css-tokens-v1-52.md new file mode 100644 index 0000000..c6fc0f6 --- /dev/null +++ b/.changeset/rename-bui-css-tokens-v1-52.md @@ -0,0 +1,5 @@ +--- +'@backstage/rename-bui-css-tokens-v1-52': minor +--- + +Add codemod to rename deprecated BUI semantic color tokens for Backstage 1.52.0 diff --git a/.changeset/v1-52-0-migration-recipe.md b/.changeset/v1-52-0-migration-recipe.md new file mode 100644 index 0000000..f65f2b0 --- /dev/null +++ b/.changeset/v1-52-0-migration-recipe.md @@ -0,0 +1,5 @@ +--- +'@backstage/v1-52-0-migration-recipe': minor +--- + +Add v1.52.0 migration recipe that runs every @backstage v1.52.0 codemod in a safe order diff --git a/README.md b/README.md index 4552c3a..b4d075f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,18 @@ See the [Codemod docs](https://docs.codemod.com) for more on building and runnin +### v1.52.0 + +Run the [`migration-recipe`](./codemods/v1.52.0/migration-recipe) to apply every codemod below in one pass, or run any individual codemod on its own. + +| Codemod | Description | +| ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| [migrate-bui-props-to-intersection](./codemods/v1.52.0/migrate-bui-props-to-intersection) | Migrate ComboboxProps/SelectProps interface extends to type intersection | +| [migrate-select-combobox-deprecated-props](./codemods/v1.52.0/migrate-select-combobox-deprecated-props) | Migrate deprecated Select/Combobox search props and option value to id | +| [migration-recipe](./codemods/v1.52.0/migration-recipe) | Migration recipe that runs every @backstage v1.52.0 codemod from the registry in a safe order. | +| [remove-stitching-strategy-mode](./codemods/v1.52.0/remove-stitching-strategy-mode) | Remove deprecated catalog.stitchingStrategy.mode from app-config | +| [rename-bui-css-tokens-v1-52](./codemods/v1.52.0/rename-bui-css-tokens-v1-52) | Rename deprecated BUI semantic color tokens | + ### v1.51.0 Run the [`migration-recipe`](./codemods/v1.51.0/migration-recipe) to apply every codemod below in one pass, or run any individual codemod on its own. @@ -26,25 +38,6 @@ Run the [`migration-recipe`](./codemods/v1.51.0/migration-recipe) to apply every | [rename-header-main-class](./codemods/v1.51.0/rename-header-main-class) | Rename removed .bui-Header to .bui-HeaderContent and classNames.root to classNames.content | | [render-test-app-nav-migration](./codemods/v1.51.0/render-test-app-nav-migration) | Migrate renderInTestApp nav-item tests to renderTestApp for Backstage 1.51.0 | -### v1.50.0 - -Run the [`migration-recipe`](./codemods/v1.50.0/migration-recipe) to apply every codemod below in one pass, or run any individual codemod on its own. - -| Codemod | Description | -| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| [add-entity-ref-to-location](./codemods/v1.50.0/add-entity-ref-to-location) | Add required entityRef field to Location object literals from @backstage/catalog-client | -| [add-update-location-method](./codemods/v1.50.0/add-update-location-method) | Add required updateLocation method to CatalogApi and CatalogService implementations | -| [catalog-node-alpha-to-stable](./codemods/v1.50.0/catalog-node-alpha-to-stable) | Replace deprecated @backstage/plugin-catalog-node/alpha exports with stable equivalents | -| [dialog-api-show-to-open](./codemods/v1.50.0/dialog-api-show-to-open) | Replace deprecated DialogApi .show() and .showModal() with .open() | -| [header-tab-to-nav-tab-item](./codemods/v1.50.0/header-tab-to-nav-tab-item) | Rename HeaderTab to HeaderNavTabItem and remove matchStrategy property in @backstage/ui | -| [humanize-entity-ref-to-presentation](./codemods/v1.50.0/humanize-entity-ref-to-presentation) | Replace deprecated humanizeEntityRef/humanizeEntity with Catalog Presentation API | -| [migrate-permissioned-route](./codemods/v1.50.0/migrate-permissioned-route) | Migrate PermissionedRoute to Route + RequirePermission for @backstage/plugin-permission-react | -| [migrate-signals-service](./codemods/v1.50.0/migrate-signals-service) | Rename deprecated SignalService exports to SignalsService in @backstage/plugin-signals-node | -| [migration-recipe](./codemods/v1.50.0/migration-recipe) | Migration recipe that runs every @backstage v1.50.0 codemod from the registry in a safe order. | -| [remove-bootstrap-env-proxy](./codemods/v1.50.0/remove-bootstrap-env-proxy) | Remove deprecated bootstrapEnvProxyAgents() call and import from @backstage/cli-common | -| [rename-plugin-header-toolbar](./codemods/v1.50.0/rename-plugin-header-toolbar) | Rename .bui-PluginHeaderToolbarWrapper to .bui-PluginHeaderToolbar and classNames.toolbarWrapper to classNames.toolbar | -| [replace-create-schema-from-zod](./codemods/v1.50.0/replace-create-schema-from-zod) | Replace createSchemaFromZod and config.schema with configSchema | - Older versions are available in the [`codemods/`](./codemods) directory. diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/codemod.yaml b/codemods/v1.52.0/migrate-bui-props-to-intersection/codemod.yaml new file mode 100644 index 0000000..65244e0 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/codemod.yaml @@ -0,0 +1,20 @@ +schema_version: '1.0' + +name: '@backstage/migrate-bui-props-to-intersection' +version: '0.1.0' +description: 'Backstage 1.52.0: Migrate ComboboxProps/SelectProps interface extends to type intersection' +author: 'Paul Schultz' +license: 'Apache-2.0' +repository: 'https://github.com/backstage/codemods' +workflow: 'workflow.yaml' + +targets: + languages: ['tsx', 'ts', 'css'] + +keywords: ['backstage', 'migration', 'ui', 'combobox', 'select', 'props', '1.52.0'] + +registry: + access: 'public' + visibility: 'public' + +capabilities: [] diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/package.json b/codemods/v1.52.0/migrate-bui-props-to-intersection/package.json new file mode 100644 index 0000000..5ffb4c5 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/package.json @@ -0,0 +1,13 @@ +{ + "name": "@backstage/migrate-bui-props-to-intersection", + "version": "0.1.0", + "description": "Backstage 1.52.0: Migrate ComboboxProps/SelectProps interface extends to type intersection", + "type": "module", + "scripts": { + "test": "yarn exec codemod jssg test -l tsx ./scripts/codemod.ts ./tests && yarn exec codemod jssg test -l css ./scripts/codemod-css.ts ./tests-css && yarn exec codemod workflow validate -w workflow.yaml" + }, + "devDependencies": { + "@codemod.com/jssg-types": "1.5.2", + "codemod": "1.7.15" + } +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod-css.ts b/codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod-css.ts new file mode 100644 index 0000000..e1e4483 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod-css.ts @@ -0,0 +1,48 @@ +import type { Codemod, Edit } from 'codemod:ast-grep' +import type CSS from 'codemod:ast-grep/langs/css' +import { useMetricAtom } from 'codemod:metrics' + +const migrationMetric = useMetricAtom('migrate-bui-props-to-intersection') + +const TODO_COMMENT = + '/* TODO(backstage-codemod): Select popover DOM structure changed — list content is now inside .bui-PopoverContent wrapper */' + +const transform: Codemod = async (root) => { + const rootNode = root.root() + const edits: Edit[] = [] + + // Find all rule_set nodes (CSS rulesets) and check their selectors + const rulesets = rootNode.findAll({ + rule: { + kind: 'rule_set', + }, + }) + + for (const ruleset of rulesets) { + const selectors = ruleset.find({ + rule: { kind: 'selectors' }, + }) + if (!selectors) { + continue + } + + const selectorText = selectors.text() + + // Check if selector contains .bui-SelectPopover followed by > (direct child combinator) + if (!/\.bui-SelectPopover\s*>/.test(selectorText)) { + continue + } + + // Prepend the TODO comment at the same indentation as the rule set + const rulesetText = ruleset.text() + const indentMatch = rulesetText.match(/^(\s*)/) + const indent = indentMatch ? indentMatch[1] : '' + edits.push(ruleset.replace(`${indent}${TODO_COMMENT}\n${rulesetText}`)) + migrationMetric.increment({ action: 'css-selector-todo-added' }) + } + + const result = await Promise.resolve(edits.length > 0 ? rootNode.commitEdits(edits) : null) + return result +} + +export default transform diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod.ts b/codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod.ts new file mode 100644 index 0000000..cf35557 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/scripts/codemod.ts @@ -0,0 +1,254 @@ +import type { Codemod, Edit, SgNode } from 'codemod:ast-grep' +import type TSX from 'codemod:ast-grep/langs/tsx' +import { useMetricAtom } from 'codemod:metrics' + +const migrationMetric = useMetricAtom('migrate-bui-props-to-intersection') + +const TARGET_TYPES = new Set(['ComboboxProps', 'SelectProps']) +const UI_SOURCE = '@backstage/ui' + +function escapeRegex(str: string): string { + return str.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +/** + * Find import statements from a given source module. + */ +function findImportStatementsFrom(rootNode: SgNode, source: string): SgNode[] { + return rootNode.findAll({ + rule: { + kind: 'import_statement', + has: { + kind: 'string', + has: { + kind: 'string_fragment', + regex: `^${escapeRegex(source)}$`, + }, + }, + }, + }) +} + +interface ImportedTypes { + /** Named imports: local name → original name */ + localNames: Set + /** Namespace aliases from `import * as UI from '@backstage/ui'` */ + namespaceAliases: string[] +} + +/** + * Collect locally imported type names that match our target types + * (ComboboxProps, SelectProps) from @backstage/ui import statements. + * Also collects namespace aliases for `import * as UI` patterns. + */ +function collectTargetTypeNames(importStatements: SgNode[]): ImportedTypes { + const localNames = new Set() + const namespaceAliases: string[] = [] + + for (const imp of importStatements) { + // Handle namespace imports: import * as UI from '@backstage/ui' + const nsImport = imp.find({ rule: { kind: 'namespace_import' } }) + if (nsImport) { + const aliasNode = nsImport.find({ rule: { kind: 'identifier' } }) + if (aliasNode) { + namespaceAliases.push(aliasNode.text()) + } + continue + } + + // Handle named imports: import { ComboboxProps } from '@backstage/ui' + for (const spec of imp.findAll({ rule: { kind: 'import_specifier' } })) { + const identifiers = spec.findAll({ + rule: { + any: [{ kind: 'identifier' }, { kind: 'type_identifier' }], + }, + }) + + const [importedNameNode] = identifiers + if (!importedNameNode || !TARGET_TYPES.has(importedNameNode.text())) { + continue + } + + // If aliased (import { ComboboxProps as CP }), use the local alias + const localNameNode = identifiers[1] ?? importedNameNode + localNames.add(localNameNode.text()) + } + } + + return { localNames, namespaceAliases } +} + +/** + * Build the type alias replacement text from interface declaration parts. + */ +function buildTypeAlias(name: string, typeParams: string | null, extendsTypes: string[], body: string): string { + const typeParamStr = typeParams ?? '' + + // Strip outer braces and trim whitespace from body + const inner = body.replace(/^\{/, '').replace(/\}$/, '').trim() + + const parts = [...extendsTypes] + if (inner.length > 0) { + parts.push(`{\n ${inner}\n}`) + } + + return `type ${name}${typeParamStr} = ${parts.join(' & ')};` +} + +/** + * Transform interface declarations that extend ComboboxProps or SelectProps + * into type alias intersections. + */ +/** + * Check if a type node in the extends clause references a target type, + * handling both direct names and namespace-qualified names (UI.ComboboxProps). + */ +function isTargetType(child: SgNode, localNames: Set, namespaceAliases: string[]): boolean { + const kind = child.kind() + + if (kind === 'type_identifier') { + return localNames.has(child.text()) + } + + // Handle UI.ComboboxProps (nested_type_identifier in extends clause) + if (kind === 'nested_type_identifier') { + const nsNode = child.find({ rule: { kind: 'identifier' } }) + const typeNode = child.find({ rule: { kind: 'type_identifier' } }) + if (nsNode && typeNode && namespaceAliases.includes(nsNode.text()) && TARGET_TYPES.has(typeNode.text())) { + return true + } + } + + // Handle generic types like ComboboxProps or UI.ComboboxProps + if (kind === 'generic_type') { + // Check for namespace-qualified generic: UI.ComboboxProps + const nestedType = child.find({ rule: { kind: 'nested_type_identifier' } }) + if (nestedType) { + const nsNode = nestedType.find({ rule: { kind: 'identifier' } }) + const typeNode = nestedType.find({ rule: { kind: 'type_identifier' } }) + if (nsNode && typeNode && namespaceAliases.includes(nsNode.text()) && TARGET_TYPES.has(typeNode.text())) { + return true + } + } + // Check for direct generic: ComboboxProps + const typeIdent = child.find({ rule: { kind: 'type_identifier' } }) + return typeIdent !== null && localNames.has(typeIdent.text()) + } + + return false +} + +function transformInterfaces( + rootNode: SgNode, + localNames: Set, + namespaceAliases: string[], + edits: Edit[], +): void { + // Find all interface declarations that have an extends clause + const interfaceDecls = rootNode.findAll({ + rule: { + kind: 'interface_declaration', + has: { + kind: 'extends_type_clause', + }, + }, + }) + + for (const decl of interfaceDecls) { + const extendsClause = decl.find({ + rule: { kind: 'extends_type_clause' }, + }) + if (!extendsClause) { + continue + } + + // Collect all types listed in the extends clause + const extendsTypes: string[] = [] + let hasTargetType = false + + for (const child of extendsClause.children()) { + const kind = child.kind() + if (kind === 'type_identifier' || kind === 'generic_type' || kind === 'nested_type_identifier') { + if (isTargetType(child, localNames, namespaceAliases)) { + hasTargetType = true + } + extendsTypes.push(child.text()) + } + } + + if (!hasTargetType) { + continue + } + + // Get the interface name — it's the type_identifier direct child of interface_declaration + // (not inside the extends clause) + const allTypeIdents = decl.findAll({ rule: { kind: 'type_identifier' } }) + // The first type_identifier that is NOT inside the extends clause is the interface name + let interfaceName: string | null = null + for (const ident of allTypeIdents) { + // Check if this identifier is a child of the extends clause + let insideExtends = false + let parent = ident.parent() + while (parent) { + if (parent.kind() === 'extends_type_clause') { + insideExtends = true + break + } + if (parent.kind() === 'interface_declaration') { + break + } + parent = parent.parent() + } + if (!insideExtends) { + interfaceName = ident.text() + break + } + } + if (!interfaceName) { + continue + } + + // Get type parameters if present + const typeParamsNode = decl.find({ rule: { kind: 'type_parameters' } }) + const typeParams = typeParamsNode ? typeParamsNode.text() : null + + // Get the interface body + const bodyNode = decl.find({ rule: { kind: 'interface_body' } }) + if (!bodyNode) { + continue + } + + const replacement = buildTypeAlias(interfaceName, typeParams, extendsTypes, bodyNode.text()) + + edits.push(decl.replace(replacement)) + migrationMetric.increment({ + action: 'interface-to-type', + interface: interfaceName, + }) + } +} + +const transform: Codemod = async (root) => { + const rootNode = root.root() + const edits: Edit[] = [] + + // Step 1: Find imports from @backstage/ui + const uiImports = findImportStatementsFrom(rootNode, UI_SOURCE) + if (uiImports.length === 0) { + return null + } + + // Step 2: Collect locally imported ComboboxProps / SelectProps names + namespace aliases + const { localNames, namespaceAliases } = collectTargetTypeNames(uiImports) + if (localNames.size === 0 && namespaceAliases.length === 0) { + return null + } + + // Step 3: Transform matching interface declarations + transformInterfaces(rootNode, localNames, namespaceAliases, edits) + + const result = await Promise.resolve(edits.length > 0 ? rootNode.commitEdits(edits) : null) + return result +} + +export default transform diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/expected.css b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/expected.css new file mode 100644 index 0000000..b75edae --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/expected.css @@ -0,0 +1,3 @@ +.bui-SelectPopover { + border-radius: 4px; +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/input.css b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/input.css new file mode 100644 index 0000000..b75edae --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/noop-no-child-combinator/input.css @@ -0,0 +1,3 @@ +.bui-SelectPopover { + border-radius: 4px; +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/expected.css b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/expected.css new file mode 100644 index 0000000..3bf2a49 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/expected.css @@ -0,0 +1,8 @@ +/* TODO(backstage-codemod): Select popover DOM structure changed — list content is now inside .bui-PopoverContent wrapper */ +.bui-SelectPopover > .list-item { + padding: 8px; +} + +.bui-SelectPopover { + border: 1px solid var(--bui-border-2); +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/input.css b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/input.css new file mode 100644 index 0000000..ae19cac --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/input.css @@ -0,0 +1,7 @@ +.bui-SelectPopover > .list-item { + padding: 8px; +} + +.bui-SelectPopover { + border: 1px solid var(--bui-border-2); +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/metrics.json b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/metrics.json new file mode 100644 index 0000000..96af3b9 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests-css/select-popover-child/metrics.json @@ -0,0 +1,10 @@ +{ + "migrate-bui-props-to-intersection": [ + { + "cardinality": { + "action": "css-selector-todo-added" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/expected.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/expected.tsx new file mode 100644 index 0000000..df6ade4 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/expected.tsx @@ -0,0 +1,3 @@ +import { ComboboxProps } from '@backstage/ui'; + +type MyComboboxProps = ComboboxProps; diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/input.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/input.tsx new file mode 100644 index 0000000..5014d23 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/input.tsx @@ -0,0 +1,3 @@ +import { ComboboxProps } from '@backstage/ui'; + +interface MyComboboxProps extends ComboboxProps {} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/metrics.json b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/metrics.json new file mode 100644 index 0000000..52d41f0 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/empty-body/metrics.json @@ -0,0 +1,11 @@ +{ + "migrate-bui-props-to-intersection": [ + { + "cardinality": { + "action": "interface-to-type", + "interface": "MyComboboxProps" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/expected.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/expected.tsx new file mode 100644 index 0000000..5012902 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/expected.tsx @@ -0,0 +1,9 @@ +import { SelectProps } from '@backstage/ui'; + +interface WithAnalytics { + trackingId: string; +} + +type MySelectProps = SelectProps & WithAnalytics & { + customFilter: boolean; +}; diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/input.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/input.tsx new file mode 100644 index 0000000..9704372 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/input.tsx @@ -0,0 +1,9 @@ +import { SelectProps } from '@backstage/ui'; + +interface WithAnalytics { + trackingId: string; +} + +interface MySelectProps extends SelectProps, WithAnalytics { + customFilter: boolean; +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/metrics.json b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/metrics.json new file mode 100644 index 0000000..f80c9fd --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/multi-extends/metrics.json @@ -0,0 +1,11 @@ +{ + "migrate-bui-props-to-intersection": [ + { + "cardinality": { + "action": "interface-to-type", + "interface": "MySelectProps" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/expected.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/expected.tsx new file mode 100644 index 0000000..e9c01b6 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/expected.tsx @@ -0,0 +1,5 @@ +import { ComboboxProps } from '@backstage/ui'; + +type MyComboboxProps = ComboboxProps & { + trackingId: string; +}; diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/input.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/input.tsx new file mode 100644 index 0000000..9f8589b --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/input.tsx @@ -0,0 +1,5 @@ +import { ComboboxProps } from '@backstage/ui'; + +interface MyComboboxProps extends ComboboxProps { + trackingId: string; +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/metrics.json b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/metrics.json new file mode 100644 index 0000000..52d41f0 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/named-import/metrics.json @@ -0,0 +1,11 @@ +{ + "migrate-bui-props-to-intersection": [ + { + "cardinality": { + "action": "interface-to-type", + "interface": "MyComboboxProps" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/expected.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/expected.tsx new file mode 100644 index 0000000..eb2eda0 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/expected.tsx @@ -0,0 +1,5 @@ +import * as UI from '@backstage/ui'; + +type MyComboboxProps = UI.ComboboxProps & { + trackingId: string; +}; diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/input.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/input.tsx new file mode 100644 index 0000000..065d8fb --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/input.tsx @@ -0,0 +1,5 @@ +import * as UI from '@backstage/ui'; + +interface MyComboboxProps extends UI.ComboboxProps { + trackingId: string; +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/metrics.json b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/metrics.json new file mode 100644 index 0000000..52d41f0 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/namespace-import/metrics.json @@ -0,0 +1,11 @@ +{ + "migrate-bui-props-to-intersection": [ + { + "cardinality": { + "action": "interface-to-type", + "interface": "MyComboboxProps" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/expected.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/expected.tsx new file mode 100644 index 0000000..773adf3 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/expected.tsx @@ -0,0 +1,5 @@ +import { SelectProps } from 'react-aria'; + +interface MySelectProps extends SelectProps { + customOption: string; +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/input.tsx b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/input.tsx new file mode 100644 index 0000000..773adf3 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tests/noop-non-bui/input.tsx @@ -0,0 +1,5 @@ +import { SelectProps } from 'react-aria'; + +interface MySelectProps extends SelectProps { + customOption: string; +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/tsconfig.json b/codemods/v1.52.0/migrate-bui-props-to-intersection/tsconfig.json new file mode 100644 index 0000000..6a30022 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["@codemod.com/jssg-types"], + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true + }, + "exclude": ["tests", "tests-css", "node_modules"] +} diff --git a/codemods/v1.52.0/migrate-bui-props-to-intersection/workflow.yaml b/codemods/v1.52.0/migrate-bui-props-to-intersection/workflow.yaml new file mode 100644 index 0000000..a8f4b27 --- /dev/null +++ b/codemods/v1.52.0/migrate-bui-props-to-intersection/workflow.yaml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: '1' + +nodes: + - id: apply-transforms + name: 'Apply AST Transformations' + type: automatic + steps: + - name: 'Migrate interface extends ComboboxProps/SelectProps to type intersection' + js-ast-grep: + js_file: scripts/codemod.ts + language: 'tsx' + semantic_analysis: file + include: + - '**/*.ts' + - '**/*.tsx' + exclude: + - '**/node_modules/**' + + - name: 'Add TODO comments to .bui-SelectPopover > CSS selectors' + js-ast-grep: + js_file: scripts/codemod-css.ts + language: 'css' + include: + - '**/*.css' + - '**/*.scss' + exclude: + - '**/node_modules/**' diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/codemod.yaml b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/codemod.yaml new file mode 100644 index 0000000..afda364 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/codemod.yaml @@ -0,0 +1,20 @@ +schema_version: '1.0' + +name: '@backstage/migrate-select-combobox-deprecated-props' +version: '0.1.0' +description: 'Backstage 1.52.0: Migrate deprecated Select/Combobox search props and option value to id' +author: 'Paul Schultz' +license: 'Apache-2.0' +repository: 'https://github.com/backstage/codemods' +workflow: 'workflow.yaml' + +targets: + languages: ['tsx', 'ts'] + +keywords: ['backstage', 'migration', 'ui', 'select', 'combobox', 'props', '1.52.0'] + +registry: + access: 'public' + visibility: 'public' + +capabilities: [] diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/package.json b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/package.json new file mode 100644 index 0000000..bdcf326 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/package.json @@ -0,0 +1,13 @@ +{ + "name": "@backstage/migrate-select-combobox-deprecated-props", + "version": "0.1.0", + "description": "Backstage 1.52.0: Migrate deprecated Select/Combobox search props and option value to id", + "type": "module", + "scripts": { + "test": "yarn exec codemod jssg test -l tsx ./scripts/codemod.ts ./tests && yarn exec codemod workflow validate -w workflow.yaml" + }, + "devDependencies": { + "@codemod.com/jssg-types": "1.5.2", + "codemod": "1.7.15" + } +} diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/scripts/codemod.ts b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/scripts/codemod.ts new file mode 100644 index 0000000..c6345e5 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/scripts/codemod.ts @@ -0,0 +1,383 @@ +import type { Codemod, Edit, SgNode } from 'codemod:ast-grep' +import type TSX from 'codemod:ast-grep/langs/tsx' +import { useMetricAtom } from 'codemod:metrics' + +const migrationMetric = useMetricAtom('migrate-select-combobox-deprecated-props') + +const UI_SOURCE = '@backstage/ui' +const SELECT_COMPONENTS = new Set(['Select']) +const COMBOBOX_COMPONENTS = new Set(['Combobox']) +const ALL_COMPONENTS = new Set([...SELECT_COMPONENTS, ...COMBOBOX_COMPONENTS]) + +/** Select deprecated search props */ +const SELECT_SEARCH_PROPS = new Set(['searchable', 'searchPlaceholder']) +/** Combobox deprecated search props */ +const COMBOBOX_SEARCH_PROPS = new Set(['inputValue', 'onInputChange']) + +function escapeRegex(str: string): string { + return str.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +// --------------------------------------------------------------------------- +// Import analysis (mirrors loading-to-is-pending pattern) +// --------------------------------------------------------------------------- + +function findImportStatementsFrom(rootNode: SgNode, source: string): SgNode[] { + return rootNode.findAll({ + rule: { + kind: 'import_statement', + has: { + kind: 'string', + has: { + kind: 'string_fragment', + regex: `^${escapeRegex(source)}$`, + }, + }, + }, + }) as SgNode[] +} + +interface ImportedComponents { + /** Map of localName → originalName (e.g. "MySelect" → "Select") */ + localNames: Map + namespaceAliases: string[] +} + +function collectImportedComponents(importStatements: SgNode[]): ImportedComponents { + const localNames = new Map() + const namespaceAliases: string[] = [] + + for (const imp of importStatements) { + const nsImport = imp.find({ rule: { kind: 'namespace_import' } }) + if (nsImport) { + const aliasNode = nsImport.find({ rule: { kind: 'identifier' } }) + if (aliasNode) { + namespaceAliases.push(aliasNode.text()) + } + continue + } + + for (const spec of imp.findAll({ rule: { kind: 'import_specifier' } })) { + const identifiers = spec.findAll({ + rule: { + any: [{ kind: 'identifier' }, { kind: 'type_identifier' }], + }, + }) + + const [importedNameNode] = identifiers + if (!importedNameNode || !ALL_COMPONENTS.has(importedNameNode.text())) { + continue + } + + const localNameNode = identifiers[1] ?? importedNameNode + localNames.set(localNameNode.text(), importedNameNode.text()) + } + } + + return { localNames, namespaceAliases } +} + +// --------------------------------------------------------------------------- +// JSX element helpers (mirrors loading-to-is-pending pattern) +// --------------------------------------------------------------------------- + +function getOpeningElement(el: SgNode): SgNode | null { + if (el.is('jsx_self_closing_element')) { + return el + } + if (el.is('jsx_element')) { + const opening = el.child(0) + return opening?.is('jsx_opening_element') ? opening : null + } + return null +} + +function getComponentNameNode(opening: SgNode): SgNode | null { + return opening.child(1) ?? null +} + +function getOriginalComponentName( + nameNode: SgNode, + localNames: Map, + namespaceAliases: string[], +): string | null { + if (nameNode.is('identifier')) { + return localNames.get(nameNode.text()) ?? null + } + + if (nameNode.is('member_expression')) { + const objNode = nameNode.child(0) + const propNode = nameNode.find({ rule: { kind: 'property_identifier' } }) + if ( + objNode?.is('identifier') === true && + namespaceAliases.includes(objNode.text()) && + propNode !== null && + ALL_COMPONENTS.has(propNode.text()) + ) { + return propNode.text() + } + } + + return null +} + +// --------------------------------------------------------------------------- +// JSX attribute helpers +// --------------------------------------------------------------------------- + +function getAttrName(attr: SgNode): string | null { + const propId = attr.find({ rule: { kind: 'property_identifier' } }) + return propId ? propId.text() : null +} + +/** + * Return the expression text for a JSX attribute value. + * - Boolean shorthand (`searchable`) → returns `null` + * - String literal (`searchPlaceholder="text"`) → returns the full quoted string + * - Expression (`inputValue={query}`) → returns the inner expression text + */ +function getAttrValueExpression(attr: SgNode): string | null { + // Boolean shorthand: + ); +} diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/existing-search-prop/input.tsx b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/existing-search-prop/input.tsx new file mode 100644 index 0000000..9d23eb7 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/existing-search-prop/input.tsx @@ -0,0 +1,7 @@ +import { Select } from '@backstage/ui'; + +export function OwnerFilter() { + return ( + + ); +} diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/inline-options-value-to-id/input.tsx b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/inline-options-value-to-id/input.tsx new file mode 100644 index 0000000..6806c45 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/inline-options-value-to-id/input.tsx @@ -0,0 +1,12 @@ +import { Select } from '@backstage/ui'; + +export function StatusFilter() { + return ( + + ); +} diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/noop-non-bui/input.tsx b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/noop-non-bui/input.tsx new file mode 100644 index 0000000..9168b37 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/noop-non-bui/input.tsx @@ -0,0 +1,7 @@ +import { Select } from 'custom-ui-library'; + +export function CustomFilter() { + return ( + + ); +} diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/select-searchable/input.tsx b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/select-searchable/input.tsx new file mode 100644 index 0000000..164d3e7 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/select-searchable/input.tsx @@ -0,0 +1,7 @@ +import { Select } from '@backstage/ui'; + +export function OwnerFilter() { + return ( + ; +} diff --git a/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/variable-options-todo/input.tsx b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/variable-options-todo/input.tsx new file mode 100644 index 0000000..e151ac8 --- /dev/null +++ b/codemods/v1.52.0/migrate-select-combobox-deprecated-props/tests/variable-options-todo/input.tsx @@ -0,0 +1,7 @@ +import { Select } from '@backstage/ui'; + +const statuses = [{ value: 'open', label: 'Open' }]; + +export function StatusFilter() { + return