Skip to content

Commit

Permalink
feat: Component filter for packages
Browse files Browse the repository at this point in the history
  • Loading branch information
doosuu committed Sep 8, 2023
1 parent 566577c commit 5083d8e
Show file tree
Hide file tree
Showing 14 changed files with 183 additions and 144 deletions.
46 changes: 44 additions & 2 deletions docs/PROJECT-CONFIG.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,55 @@
# Project configuration

The project configuration describes which packages your project is using and in which version. The versions of the referenced packages can be upgraded using the `upgrade` command. If you only want to see which new versions are available use `upgrade --dry-run`. Each package may expose variables which need to be set from the project configuration. If multiple different packages all expose the same named variable `foo`, setting this variable once in the project configuration will pass the value to all packages.

Read more about variables [here](./features/VARIABLES.md).

```json
{
"packages": [
{
"repo": "package-A",
"version": "v1.0.0"
},
{
"repo": "package-B",
"version": "v2.3.1-dev"
}
],
"variables": {
"repoUrl": "https://github.com/eclipse-velocitas/cli",
"copyrightYear": 2023,
"autoGenerateVehicleModel": true
}
}
```

## Components

A package always exposes 1 to n *components* each of which should provide distinct functionality, i.e. to set up a devContainer or to integrate with Github.

By default, all components of a package will be used, but if desired the used components can be filtered. By providing a list of component configurations in the project configuration:

```json
{
"packages": [
{
"name": "package-A",
"repo": "package-A",
"version": "v1.0.0"
},
{
"name": "package-B",
"repo": "package-B",
"version": "v2.3.1-dev"
}
],
"components": [
{
"id": "component-exposed-by-pkg-a"
},
{
"id": "component-exposed-by-pkg-b"
},
],
"variables": {
"repoUrl": "https://github.com/eclipse-velocitas/cli",
"copyrightYear": 2023,
Expand All @@ -20,6 +58,10 @@
}
```

The project above will only used the components `component-exposed-by-pkg-a` and `component-exposed-by-pkg-b`, ignoring any other components exposed by the packages.

## File Structure

### `packages` - Array[[`PackageConfig`](#packageconfig)]

Array of packages used in the project.
Expand Down
11 changes: 8 additions & 3 deletions src/commands/exec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,16 @@ Executing script...

const appManifestData = readAppManifest();

const [packageConfig, componentConfig, component] = findComponentByName(projectConfig, args.component);
const componentContext = findComponentByName(projectConfig, args.component);

const variables = VariableCollection.build(projectConfig, packageConfig, componentConfig, component);
const variables = VariableCollection.build(
projectConfig,
componentContext.packageReference,
componentContext.config,
componentContext.manifest,
);

const envVars = createEnvVars(packageConfig.getPackageDirectoryWithVersion(), variables, appManifestData);
const envVars = createEnvVars(componentContext.packageReference.getPackageDirectoryWithVersion(), variables, appManifestData);

try {
await runExecSpec(execSpec, args.component, projectConfig, envVars, { verbose: flags.verbose });
Expand Down
2 changes: 1 addition & 1 deletion src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async function runPostInitHook(

console.log(`... > Running post init hook for '${component.id}'`);

const maybeComponentConfig = packageConfig.components?.find((c) => c.id === component.id);
const maybeComponentConfig = projectConfig.components?.find((c) => c.id === component.id);
const componentConfig = maybeComponentConfig ? maybeComponentConfig : new ComponentConfig();
const variables = VariableCollection.build(projectConfig, packageConfig, componentConfig, component);

Expand Down
1 change: 0 additions & 1 deletion src/commands/package/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ $ velocitas component --get-path devenv-runtime-local
this.log(`${' '.repeat(4)}components:`);
for (const component of packageManifest.components) {
this.log(`${' '.repeat(5)} - id: ${component.id}`);
this.log(`${' '.repeat(8)}type: ${component.type}`);
if (component.variables && component.variables.length > 0) {
this.log(`${' '.repeat(8)}variables:`);
for (const exposedVariable of component.variables) {
Expand Down
23 changes: 13 additions & 10 deletions src/commands/sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// SPDX-License-Identifier: Apache-2.0

import { Command } from '@oclif/core';
import { ComponentType, findComponentsByType, getComponentConfig, SetupComponent } from '../../modules/component';
import { getComponentConfig, getComponents } from '../../modules/component';
import { ProjectConfig } from '../../modules/project-config';
import { installComponent } from '../../modules/setup';
import { VariableCollection } from '../../modules/variables';
Expand All @@ -22,22 +22,25 @@ export default class Sync extends Command {
static description = 'Syncs Velocitas components into your repo.';

static examples = [
`$ velocitas update MyAwesomeApp --lang cpp
`$ velocitas sync
Syncing Velocitas components!
... syncing 'devenv-github-workflows'
... syncing 'devenv-github-templates'`,
... syncing 'github-workflows'
... syncing 'github-templates'`,
];

async run(): Promise<void> {
this.log(`Syncing Velocitas components!`);
const projectConfig = ProjectConfig.read();
const setupComponents = findComponentsByType(projectConfig, ComponentType.setup);
for (const setupComponent of setupComponents) {
this.log(`... syncing '${setupComponent[0].getPackageName()}'`);

const componentConfig = getComponentConfig(setupComponent[0], setupComponent[2].id);
const variables = VariableCollection.build(projectConfig, setupComponent[0], componentConfig, setupComponent[2]);
installComponent(setupComponent[0], setupComponent[2] as SetupComponent, variables);
for (const component of getComponents(projectConfig)) {
if (!component.manifest.files || component.manifest.files.length === 0) {
continue;
}

this.log(`... syncing '${component.manifest.id}'`);
const componentConfig = getComponentConfig(projectConfig, component.manifest.id);
const variables = VariableCollection.build(projectConfig, component.packageReference, componentConfig, component.manifest);
installComponent(component.packageReference, component.manifest, variables);
}
}
}
172 changes: 91 additions & 81 deletions src/modules/component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robert Bosch GmbH
// Copyright (c) 2022-2023 Robert Bosch GmbH
//
// This program and the accompanying materials are made available under the
// terms of the Apache License, Version 2.0 which is available at
Expand All @@ -12,44 +12,70 @@
//
// SPDX-License-Identifier: Apache-2.0

import { getComponentByType, PackageConfig, PackageManifest } from './package';
import { PackageConfig } from './package';
import { ComponentConfig, ProjectConfig } from './project-config';
import { VariableDefinition } from './variables';

type IComponent = new () => { readonly type: ComponentType };

const subcomponentTypes: Record<string, IComponent> = {};

function serializable<T extends IComponent>(constructor: T) {
subcomponentTypes[new constructor().type] = constructor;
return constructor;
}

/**
* Specification of a program that is exported by a component to be used via `velocitas exec`.
*/
export interface ProgramSpec {
// Unique ID of the program. Needs to be unique within one component.
id: string;

// Short description of the program and that it is doing.
description?: string;

// Path to the executable (relative to the package root) of the exposed program.
executable: string;

// Default arguments passed to the invoked program upon execution.
args?: Array<string>;
}

/**
* Execution specification which invokes an exposed program and is able to model
* execution dependencies as well as successful startup.
*/
export interface ExecSpec {
// Reference to the id of the exposed program.
ref: string;

// Additional arguments to be passed to the exposed program.
args?: Array<string>;

// Regular expression which identifies a successful startup of the program.
startupLine?: string;

// A reference to another exposed program that this one depends on.
dependsOn?: string;
}

export enum ComponentType {
runtime = 'runtime',
deployment = 'deployment',
setup = 'setup',
/**
* File copy specification. Describes a file or set of files to be copied from a
* source to a destination.
*/
export interface FileSpec {
// The source file path or directory path (relative to the package root).
src: string;

// The destination file path or directory path (relative to the workspace root).
dst: string;

// The condition which has to be fulfilled for the copy to execute. Is evaluated as JS condition.
condition: string;
}

// Interface definition for implementing components
/**
* Interface definition for implementing components
*/
export interface Component {
// Unique ID of the component. Needs to be unique over all installed components.
id: string;

// A list of files that need to be copied from source to target when running `velocitas sync`.
files?: Array<FileSpec>;

// A list of all variable definitions exposed by this component.
variables?: Array<VariableDefinition>;

Expand All @@ -58,70 +84,29 @@ export interface Component {

// Hook which is called after the component has been initialized.
onPostInit?: Array<ExecSpec>;

// The type of the component.
readonly type: ComponentType;
}

@serializable
export class RuntimeComponent implements Component {
id = '';
alias = '';
readonly type = ComponentType.runtime;
programs? = new Array<ProgramSpec>();
onPostInit? = new Array<ExecSpec>();
variables? = new Array<VariableDefinition>();
}

export interface FileSpec {
src: string;
dst: string;
condition: string;
}

@serializable
export class SetupComponent implements Component {
id = '';
readonly type = ComponentType.setup;
files? = new Array<FileSpec>();
programs? = new Array<ProgramSpec>();
onPostInit? = new Array<ExecSpec>();
variables? = new Array<VariableDefinition>();
}
/** The context in which a component is used. It holds all necessary information to operate on a component. */
export class ComponentContext {
public packageReference: PackageConfig;
public manifest: Component;
public config: ComponentConfig;

@serializable
export class DeployComponent implements Component {
id = '';
alias = '';
readonly type = ComponentType.deployment;
programs? = new Array<ProgramSpec>();
onPostInit? = new Array<ExecSpec>();
variables? = new Array<VariableDefinition>();
}

export function findComponentsByType<TComponentType extends Component>(
projectConfig: ProjectConfig,
type: ComponentType,
): Array<[PackageConfig, PackageManifest, TComponentType]> {
const result = new Array<[PackageConfig, PackageManifest, TComponentType]>();
for (const packageConfig of projectConfig.packages) {
const componentManifest = packageConfig.readPackageManifest();
try {
result.push([packageConfig, componentManifest, getComponentByType(componentManifest, type) as TComponentType]);
} catch (e) {}
constructor(packageReference: PackageConfig, manifest: Component, config: ComponentConfig) {
this.packageReference = packageReference;
this.manifest = manifest;
this.config = config;
}

return result;
}

export function findComponentByName(projectConfig: ProjectConfig, componentId: string): [PackageConfig, ComponentConfig, Component] {
let result: [PackageConfig, ComponentConfig, Component] | undefined;
export function findComponentByName(projectConfig: ProjectConfig, componentId: string): ComponentContext {
let result: ComponentContext | undefined;
for (const packageConfig of projectConfig.packages) {
const packageManifest = packageConfig.readPackageManifest();
const matchingComponent = packageManifest.components.find((c) => c.id === componentId);
const matchingComponentConfig = getComponentConfig(packageConfig, componentId);
const matchingComponentConfig = getComponentConfig(projectConfig, componentId);
if (matchingComponent) {
result = [packageConfig, matchingComponentConfig, matchingComponent];
result = new ComponentContext(packageConfig, matchingComponent, matchingComponentConfig);
break;
}
}
Expand All @@ -133,22 +118,47 @@ export function findComponentByName(projectConfig: ProjectConfig, componentId: s
return result;
}

export function getComponentConfig(packageConfig: PackageConfig, componentId: string): ComponentConfig {
/**
* Return the configuration of a component.
*
* @param projectConfig The project configuration.
* @param componentId The ID of the component.
* @returns The configuration of the component.
*/
export function getComponentConfig(projectConfig: ProjectConfig, componentId: string): ComponentConfig {
var maybeComponentConfig: ComponentConfig | undefined;
if (packageConfig.components) {
maybeComponentConfig = packageConfig.components.find((c) => c.id === componentId);
if (projectConfig.components) {
maybeComponentConfig = projectConfig.components.find((c) => c.id === componentId);
}
return maybeComponentConfig ? maybeComponentConfig : new ComponentConfig();
}

const reviver = (_: string, v: any) => {
if (typeof v === 'object' && 'type' in v && v.type in subcomponentTypes) {
return Object.assign(new subcomponentTypes[v.type](), v);
}
return v;
};

// use this to deserialize JSON instead of plain JSON.parse
export function deserializeComponentJSON(json: string) {
return JSON.parse(json, reviver);
return JSON.parse(json);
}

/**
* Return all components used by the project. If the project specifies no components explicitly,
* all components are used by default.
*
* @param projectConfig The project configuration to use as an input.
* @returns A list of all components used by the project.
*/
export function getComponents(projectConfig: ProjectConfig): Array<ComponentContext> {
var componentTuples = new Array<ComponentContext>();

const usedComponents = projectConfig.components;

for (const packageConfig of projectConfig.packages) {
const packageManifest = packageConfig.readPackageManifest();

for (const component of packageManifest.components) {
if (usedComponents.length === 0 || usedComponents.find((c) => c.id === component.id)) {
componentTuples.push(new ComponentContext(packageConfig, component, getComponentConfig(projectConfig, component.id)));
}
}
}

return componentTuples;
}
Loading

0 comments on commit 5083d8e

Please sign in to comment.