Add the extension-names codemod.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-10-04 18:53:37 +02:00
parent 01a468aee3
commit 903dbdeb7d
3 changed files with 99 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/codemods': patch
---
Added an `extension-names` codemod, which adds a `name` key to all extensions
provided by plugins. Extension names are used to provide richer context to
events captured by the new Analytics API, and may also appear in debug output
and other situations.
To apply this codemod, run `npx @backstage/codemods apply extension-names` in
the root of your Backstage monorepo.
+5
View File
@@ -25,4 +25,9 @@ export const codemods: Codemod[] = [
description:
'Updates @backstage/core imports to use @backstage/core-* imports instead.',
},
{
name: 'extension-names',
description:
'Adds a "name" property to all core extensions provided by plugins.',
},
];
@@ -0,0 +1,83 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = (file, /** @type {import('jscodeshift').API} */ api) => {
const j = api.jscodeshift;
const root = j(file.source);
// Find all variables set to *.provide() with an invocation of an extension
// creator as its first argument.
// For example: somePlugin.provide(createReactExtension({}))
const extensionCreators = [
'createReactExtension',
'createComponentExtension',
'createRoutableExtension',
];
const extensions = root
.findVariableDeclarators()
.filter(
n =>
n.node.init?.type === 'CallExpression' &&
n.node.init?.callee.type === 'MemberExpression' &&
n.node.init?.callee?.property.name === 'provide' &&
n.node.init?.arguments.length === 1 &&
extensionCreators.includes(n.node.init?.arguments[0].callee?.name),
);
// Abort if no such variable declarations exist in this file.
if (extensions.size === 0) {
return undefined;
}
// For each variable, parse out its name and apply the name as the value of
// the "name" key on the argument passed to the extension creator's argument.
// For example: createReactExtension({ name: 'Some Extension' })
let numberAffected = 0;
extensions.nodes().flatMap(node => {
const creatorArgObject = node.init?.arguments[0].arguments[0];
const hasNameKeyAlready =
creatorArgObject.properties.filter(p => p.key.name === 'name').length ===
1;
const nameProp = j.objectProperty(
j.identifier('name'),
j.literal(node.id.name),
);
// Apply this change only once.
if (!hasNameKeyAlready) {
numberAffected++;
creatorArgObject.properties.unshift(nameProp);
}
});
// Abort if no changes were made.
if (numberAffected === 0) {
return undefined;
}
// Check for code styles to be applied to this file.
const useSingleQuote =
root.find(j.ImportDeclaration).nodes()[0]?.source.extra.raw[0] === "'";
const useTrailingComma = root
.find(j.ObjectExpression)
.nodes()[0]
?.extra?.hasOwnProperty('trailingComma');
return root.toSource({
quote: useSingleQuote ? 'single' : 'double',
trailingComma: useTrailingComma,
});
};