Use opaque types for command graph nodes
Switch CommandGraph and CliInitializer to use OpaqueCommandTreeNode and OpaqueCommandLeafNode from @internal/cli instead of raw $$type markers. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -15,8 +15,13 @@
|
||||
*/
|
||||
|
||||
import { CommandGraph } from './CommandGraph';
|
||||
import { OpaqueCliPlugin } from '@internal/cli';
|
||||
import type { BackstageCommand, CliPlugin } from '@backstage/cli-plugin-api';
|
||||
import {
|
||||
OpaqueCliPlugin,
|
||||
OpaqueCommandTreeNode,
|
||||
OpaqueCommandLeafNode,
|
||||
} from '@internal/cli';
|
||||
import type { CommandNode } from '@internal/cli';
|
||||
import type { CliPlugin } from '@backstage/cli-plugin-api';
|
||||
import { CommandRegistry } from './CommandRegistry';
|
||||
import { Command } from 'commander';
|
||||
import { version } from './version';
|
||||
@@ -25,15 +30,13 @@ import { exitWithError } from './errors';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { isPromise } from 'node:util/types';
|
||||
|
||||
function isNodeHidden(
|
||||
node:
|
||||
| { $$type: '@tree/leaf'; command: BackstageCommand }
|
||||
| { $$type: '@tree/root'; children: unknown[] },
|
||||
): boolean {
|
||||
if (node.$$type === '@tree/leaf') {
|
||||
return !!node.command.deprecated || !!node.command.experimental;
|
||||
function isNodeHidden(node: CommandNode): boolean {
|
||||
if (OpaqueCommandLeafNode.isType(node)) {
|
||||
const { command } = OpaqueCommandLeafNode.toInternal(node);
|
||||
return !!command.deprecated || !!command.experimental;
|
||||
}
|
||||
return node.children.every(child => isNodeHidden(child as any));
|
||||
const { children } = OpaqueCommandTreeNode.toInternal(node);
|
||||
return children.every(child => isNodeHidden(child));
|
||||
}
|
||||
|
||||
type UninitializedFeature = CliPlugin | Promise<{ default: CliPlugin }>;
|
||||
@@ -92,25 +95,28 @@ export class CliInitializer {
|
||||
}));
|
||||
while (queue.length) {
|
||||
const { node, argParser } = queue.shift()!;
|
||||
if (node.$$type === '@tree/root') {
|
||||
if (OpaqueCommandTreeNode.isType(node)) {
|
||||
const internal = OpaqueCommandTreeNode.toInternal(node);
|
||||
const treeParser = argParser
|
||||
.command(`${node.name} [command]`, {
|
||||
.command(`${internal.name} [command]`, {
|
||||
hidden: isNodeHidden(node),
|
||||
})
|
||||
.description(node.name);
|
||||
.description(internal.name);
|
||||
|
||||
queue.push(
|
||||
...node.children.map(child => ({
|
||||
...internal.children.map(child => ({
|
||||
node: child,
|
||||
argParser: treeParser,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
const internal = OpaqueCommandLeafNode.toInternal(node);
|
||||
argParser
|
||||
.command(node.name, {
|
||||
hidden: !!node.command.deprecated || !!node.command.experimental,
|
||||
.command(internal.name, {
|
||||
hidden:
|
||||
!!internal.command.deprecated || !!internal.command.experimental,
|
||||
})
|
||||
.description(node.command.description)
|
||||
.description(internal.command.description)
|
||||
.helpOption(false)
|
||||
.allowUnknownOption(true)
|
||||
.allowExcessArguments(true)
|
||||
@@ -129,7 +135,7 @@ export class CliInitializer {
|
||||
// Skip the command name
|
||||
if (
|
||||
argIndex === index &&
|
||||
node.command.path[argIndex] === nonProcessArgs[argIndex]
|
||||
internal.command.path[argIndex] === nonProcessArgs[argIndex]
|
||||
) {
|
||||
index += 1;
|
||||
continue;
|
||||
@@ -139,15 +145,15 @@ export class CliInitializer {
|
||||
const context = {
|
||||
args: [...positionalArgs, ...args.unknown],
|
||||
info: {
|
||||
usage: [programName, ...node.command.path].join(' '),
|
||||
name: node.command.path.join(' '),
|
||||
usage: [programName, ...internal.command.path].join(' '),
|
||||
name: internal.command.path.join(' '),
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof node.command.execute === 'function') {
|
||||
await node.command.execute(context);
|
||||
if (typeof internal.command.execute === 'function') {
|
||||
await internal.command.execute(context);
|
||||
} else {
|
||||
const mod = await node.command.execute.loader();
|
||||
const mod = await internal.command.execute.loader();
|
||||
// Handle CJS double-wrapping of default exports
|
||||
const fn =
|
||||
typeof mod.default === 'function'
|
||||
|
||||
@@ -13,27 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
CommandNode,
|
||||
OpaqueCommandTreeNode,
|
||||
OpaqueCommandLeafNode,
|
||||
} from '@internal/cli';
|
||||
import { BackstageCommand } from './types';
|
||||
|
||||
type Node = TreeNode | LeafNode;
|
||||
|
||||
interface TreeNode {
|
||||
$$type: '@tree/root';
|
||||
name: string;
|
||||
children: TreeNode[];
|
||||
}
|
||||
|
||||
interface LeafNode {
|
||||
$$type: '@tree/leaf';
|
||||
name: string;
|
||||
command: BackstageCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* A sparse graph of commands.
|
||||
*/
|
||||
export class CommandGraph {
|
||||
private graph: Node[] = [];
|
||||
private graph: CommandNode[] = [];
|
||||
|
||||
/**
|
||||
* Adds a command to the graph. The graph is sparse, so we use the path to determine the nodes
|
||||
@@ -44,28 +35,44 @@ export class CommandGraph {
|
||||
let current = this.graph;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const name = path[i];
|
||||
let next = current.find(n => n.name === name);
|
||||
let next = current.find(
|
||||
n =>
|
||||
(OpaqueCommandTreeNode.isType(n) &&
|
||||
OpaqueCommandTreeNode.toInternal(n).name === name) ||
|
||||
(OpaqueCommandLeafNode.isType(n) &&
|
||||
OpaqueCommandLeafNode.toInternal(n).name === name),
|
||||
);
|
||||
if (!next) {
|
||||
next = { $$type: '@tree/root', name, children: [] };
|
||||
next = OpaqueCommandTreeNode.createInstance('v1', {
|
||||
name,
|
||||
children: [],
|
||||
});
|
||||
current.push(next);
|
||||
} else if (next.$$type === '@tree/leaf') {
|
||||
} else if (OpaqueCommandLeafNode.isType(next)) {
|
||||
throw new Error(
|
||||
`Command already exists at path: "${path.slice(0, i).join(' ')}"`,
|
||||
);
|
||||
}
|
||||
current = next.children;
|
||||
current = OpaqueCommandTreeNode.toInternal(next).children;
|
||||
}
|
||||
const last = current.find(n => n.name === path[path.length - 1]);
|
||||
if (last && last.$$type === '@tree/leaf') {
|
||||
const lastName = path[path.length - 1];
|
||||
const last = current.find(n => {
|
||||
if (OpaqueCommandTreeNode.isType(n)) {
|
||||
return OpaqueCommandTreeNode.toInternal(n).name === lastName;
|
||||
}
|
||||
return OpaqueCommandLeafNode.toInternal(n).name === lastName;
|
||||
});
|
||||
if (last && OpaqueCommandLeafNode.isType(last)) {
|
||||
throw new Error(
|
||||
`Command already exists at path: "${path.slice(0, -1).join(' ')}"`,
|
||||
);
|
||||
} else {
|
||||
current.push({
|
||||
$$type: '@tree/leaf',
|
||||
name: path[path.length - 1],
|
||||
command,
|
||||
});
|
||||
current.push(
|
||||
OpaqueCommandLeafNode.createInstance('v1', {
|
||||
name: lastName,
|
||||
command,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,26 +83,37 @@ export class CommandGraph {
|
||||
let current = this.graph;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const name = path[i];
|
||||
const next = current.find(n => n.name === name);
|
||||
if (!next) {
|
||||
return undefined;
|
||||
} else if (next.$$type === '@tree/leaf') {
|
||||
const next = current.find(n => {
|
||||
if (OpaqueCommandTreeNode.isType(n)) {
|
||||
return OpaqueCommandTreeNode.toInternal(n).name === name;
|
||||
}
|
||||
return OpaqueCommandLeafNode.toInternal(n).name === name;
|
||||
});
|
||||
if (!next || OpaqueCommandLeafNode.isType(next)) {
|
||||
return undefined;
|
||||
}
|
||||
current = next.children;
|
||||
current = OpaqueCommandTreeNode.toInternal(next).children;
|
||||
}
|
||||
const last = current.find(n => n.name === path[path.length - 1]);
|
||||
if (!last || last.$$type === '@tree/root') {
|
||||
const lastName = path[path.length - 1];
|
||||
const last = current.find(n => {
|
||||
if (OpaqueCommandTreeNode.isType(n)) {
|
||||
return OpaqueCommandTreeNode.toInternal(n).name === lastName;
|
||||
}
|
||||
return OpaqueCommandLeafNode.toInternal(n).name === lastName;
|
||||
});
|
||||
if (!last || OpaqueCommandTreeNode.isType(last)) {
|
||||
return undefined;
|
||||
}
|
||||
return last?.command;
|
||||
return OpaqueCommandLeafNode.toInternal(last).command;
|
||||
}
|
||||
|
||||
atDepth(depth: number): Node[] {
|
||||
atDepth(depth: number): CommandNode[] {
|
||||
let current = this.graph;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
current = current.flatMap(n =>
|
||||
n.$$type === '@tree/root' ? n.children : [],
|
||||
OpaqueCommandTreeNode.isType(n)
|
||||
? OpaqueCommandTreeNode.toInternal(n).children
|
||||
: [],
|
||||
);
|
||||
}
|
||||
return current;
|
||||
|
||||
Reference in New Issue
Block a user